Cmobilecom AF 5.19 Developer Guide

20 Module Development

  1. Module Project
  2. Module Implementation
  3. Module License
  4. Module Permissions
  5. DataType Mapping
  6. Entity Type Names
  7. Module Menu
  8. Entity Backing Beans
  9. Entity List Backing Beans
  10. Select Item Choices
  11. Id Rules
  12. Form Designs
  13. Embedded Objects
  14. Shortcut Menu Nodes
  15. Access Control
  16. ORM Mappings
  17. Register Persistence Entity Managers
  18. Database Seed Sql
  19. Module Initialization
  20. Module Implementation For Web
  21. URL Rewriting For Web
  22. Entity Export/Import
  23. Resource Bundles
Cmobilecom AF is module pluggable. A module can be developed and added to instance types in system-config.xml.

Module Project

A module is a subproject. It has the following directory structure. Take the example HR module for instance.

moduleRootDir/
    src/
        |-- main/
            |-- java/
            |       com/cmobilecom/af/examples/hr
            |           entity/
            |               Employee.java
            |           model/
            |               EmployeeBean.java
            |           HrModule.java
            |--	resources
                    |-- conf/
                    |       ac.xml
                    |       orm.xml
                    |-- db/
                    |       mysql/seed.sql
                    |       oracle/seed.sql
                    |-- help/
                    |       EMP.html
                    |       zh/EMP.html
                    |-- import/
                    |       System.FD/1010.xml
                    |-- bundle/
                            messages.properties
                            messages_zh.properties
        |-- test/
            |-- java/
            |       com/cmobilecom/af/examples/hr/test
            |           HrModuleTest.java
            |-- resources/
                    |-- bundle
        |-- web/
            |-- java/
            |       com/cmobilecom/af/examples/hr/web
            |           model/
            |           HrModuleWeb.java

    build.gradle
A module project has main and test source sets, and web source set is needed only if the project has web specific code such as javascript or servlet. Module build.gradle specifies module version and dependencies. For example,

build.gradle


	version = rootProject.moduleVersion('1.0')

	dependencies {
		implementation project(':module-xyz')
		implementation 'group:name:version'
	}

The function rootProject.moduleVersion() is used for supporting snapshot build. Module version will be changed to snapshot version for -Psnapshot command line option. For example, version 1.0 will become 1.0-SNAPSHOT20220918 for build date 09/19/2022.

Add the module project into the root project's settings.gradle. Module project names must start with "module-". For example, module-hr. For example,

settings.gradle of root project
	rootProject.name = 'cmobilecom-af-examples-web'

	include 'app'

	include 'module-hr'
	project(':module-hr').projectDir = new File('../hr')

	include 'test'
	project(':test').projectDir = new File('../test')
The HR module is included in a web project, but the module itself does not necessarily have any web-specific code.

Module Implementation

A module needs to implement the Module interface. Usually extend AbstractModule and override some methods as needed. For the example Hr module,

public class HrModule extends AbstractModule {
	@Override
	public String getName() {
		return "HR";
	}
}
A module must have a name. In addition, a module usually has entities, backing beans for entities, permissions for access control, module menu for managing entities, etc. Override corresponding methods if needed in Module implementation.

Module License

By default, license is not required for the module to be fully functional. If license is required, override getLicenseFeatureRequired():

	@Override
	public ModuleScopedName getLicenseFeatureRequired() {
		return new ModuleScopedName(getName(), "Module");
	}
And specify the license server domain on Cmobilecom cloud who can issue license for the module. For example,
	 
	@Override
	public String getLicenseIssuerDomain() {
		return "myLicenseServer.cmobilecom.com";
	}

Module Permissions

A module defines a list of permissions for access control. Permissions referenced in module access control (e.g., conf/ac.xml) must be defined.
 
	@Override
	public List<String> getPermissions(InstanceType instanceType) {
	
	}
The returned permission names are module local names, and they will be used to create ModuleScopedName(s) so that they are unique globally. Module permissions will be inserted into the Permission table when the module is initialized.

For example, the example HR module defines the following permissions.


	@Override
	public List<String> getPermissions(InstanceType instanceType) {
		List<String> permissions = super.getPermissions(instanceType);

		permissions.addAll(getPermissionNames(ADDR, true));
		permissions.addAll(getPermissionNames(DEPT, true));
		permissions.addAll(getPermissionNames(EMP, true, AccessType.VIEW));
		permissions.addAll(getPermissionNames(EC, true, AccessType.VIEW));
		permissions.addAll(getPermissionNames(EC, "Approve"));

		return permissions;
	}

DataType Mapping

For entity types to be managed, they must be mapped to DataType(s). For example, HrModule manages Employee, ExpenseClaim and ExpenseClaimItem types, and they are mapped to type names: EMP, EC and ECI respectively. Note that the maximum length of a DataType as string(module.type) is 20. In other words, the total length of module name and type name must not be greater than 19.

	@Override
	public Map<Class, DataType> getDataTypeMapping() {
		Map<Class, DataType> classToDataTypeMap = new HashMap<>();

		classToDataTypeMap.put(Employee.class, new DataType(null, "EMP"));
		classToDataTypeMap.put(ExpenseClaim.class, new DataType(null, "EC"));
		classToDataTypeMap.put(ExpenseClaimItem.class, new DataType(null, "ECI"));
		return classToDataTypeMap;
	}
The module of DataType can be null which defaults to the module name. If an entity type is not persisted, it will not need a DataType mapping unless its access control is defined in the module access control XML.

Entity Type Names

The default names of an entity type are: To override default names, set them by overriding the following method.

	@Override
	public Map<Class, EntityTypeName> getEntityTypeNameMap() {
  		Map<Class, EntityTypeName> typeNameMap = new HashMap<>();

		typeNameMap.put(ExpenseReportQueryForm.class,
				new EntityTypeName("ExpenseReportQuery", "ExpenseReportQuery"));
		return typeNameMap;
	}
The resource bundle for current locale will be used to translate entity type names for display.

Module Menu

A module has one or more MenuNodeFactory for creating module menu. For example,
public class HrMenuNodeFactory extends ModuleMenuNodeFactory {

	public HrMenuNodeFactory(MenuBean menuBean, MenuViewConfig viewConfig, ModuleNode moduleNode) {
		super(menuBean, viewConfig, moduleNode);
	}

	@Override
	protected void createSubMenu() throws SystemException {
		MenuViewConfig viewConfig = this.viewConfig.clone();

		MenuNode settingsMenuNode = addSettingsMenuNode(rootMenuNode);

		TypeDescriptor[] setupTypes = new TypeDescriptor[] {
				new TypeDescriptor<>(Employee.class, null, viewConfig, null, true, null, null),
				getFormDesignTypeDescriptor(viewConfig),
				getIdRuleTypeDescriptor(viewConfig)
		};
		addTypedMenuNodes(this, settingsMenuNode, setupTypes);

		TypeDescriptor[] types = new TypeDescriptor[] {
			new TypeDescriptor<>(ExpenseClaim.class, null, viewConfig, null, true, null, null),
		};
		addTypedMenuNodes(this, rootMenuNode, types);
	}
}
The module menu that would be created by the factory:
	HR
	|-- Settings
	|      |-- Employees
	|      |     |-- Create
	|      |     |-- Search
	|      |-- FormDesigns
	|      |     |-- Create
	|      |     |-- Import
	|      |     |-- Import/System
	|      |     |-- Search
	|      |-- IdRules
	|            |-- Create
	|-- ExpenseClaims
	             |-- Create
	             |-- Search
MenuNodeFactory instances are created from Module implementation class:

	@Override
	public MenuNodeFactory createMenuNodeFactory(MenuBean menuBean,
			MenuViewConfig viewConfig, ModuleNode moduleNode, String factoryName) {
		return new HrMenuNodeFactory(menuBean, viewConfig, moduleNode);
	}
The method is called to build module menu for a module-node in system-config.xml. A number of factories can be specified for a module-node, and default factory(named "default") will be used if no factories are specified. For example,
<system-config>
	<instance-type id="hr" name="HR">
		<module-node module="HR"/>

		<module-node module="xyz">
			<menu-node-factory name="factoryOne"/>
		</module-node>
	</instance-type>
</system-config>
HR module uses default factory, and Module "xyz" uses the factory named "factoryOne". Module menus will be built and aggregated for each user view of a DataAccessUnit since each user can have different view options and roles for access control.

TypedMenuNodeFactoryContext and CriteriaElement can be used to customize a typed menu node. For example, create a menu node: Part-Time Employees that manages PART_TIME employees:


	TypedMenuNodeFactoryContext context = new TypedMenuNodeFactoryContext("PartTimeEmployees",
		"PartTimeEmployee", "PartTimeEmployees", false, null);
	TypeDescriptor[] types = new TypeDescriptor[] {
		new TypeDescriptor<>(Employee.class, context,
			new CriteriaElement[]{
				DetachedCriteria.eq("type", Employee.Type.PART_TIME)},
			viewConfig, null, true, null, null)
		};
	addTypedMenuNodes(this, rootMenuNode, types);

The menu node will create, query and show part-time employees only.

Entity Backing Beans

Create an EntityBackingBean subclass for each entity type that will have a UI view for user interaction including creating, viewing, editing and query. Register EntityBackingBean subclasses in the following method:
 	
	@Override
	public Map<Class<? extends PersistenceEntity>, Class<? extends EntityBackingBean>> getEntityBackingBeanMap() {
		Map<Class<? extends PersistenceEntity>, Class<? extends EntityBackingBean>> entityBackingBeanMap = new HashMap<>();

		entityBackingBeanMap.put(Employee.class, EmployeeBean.class);
		entityBackingBeanMap.put(ExpenseClaim.class, ExpenseClaimBean.class);
		entityBackingBeanMap.put(ExpenseClaimItem.class, ExpenseClaimItemBean.class);
		entityBackingBeanMap.put(ExpenseReportQueryForm.class, ExpenseReportQueryFormBean.class);
		return entityBackingBeanMap;
	}
ExpenseReportQueryForm is for reporting only and will not be persisted, but it needs an EntityBackingBean to be visible to users. If an EntityBackingBean is not found for an entity type, then search registered EntityBackingBean(s) for the superclasses of the entity type recursively.

Entity List Backing Beans

EntityListBackingBean is the default backing bean for a list of entities, supporting in-place editing, pagination and row expansion. Most of its behaviors can be customized using its EntityBackingBean.

One scenario to extend EntityListBackingBean is to add menu nodes to its header or footer menu and handle their actions. Override getEntityListBackingBeanMap() to provide the mappings from entity types to their EntityListBackingBean(s). For example,

 
	@Override
	public Map<Class<? extends PersistenceEntity>, Class<? extends EntityListBackingBean>> getEntityListBackingBeanMap() {
		Map<Class<? extends PersistenceEntity>, Class<? extends EntityListBackingBean>> entityListBackingBeanMap = new HashMap<>();

		entityListBackingBeanMap.put(Employee.class, EmployeeListBean.class);
		return entityListBackingBeanMap;
	}
Similarly if an EntityListBackingBean is not found for an entity type, then search registered EntityListBackingBean(s) for the superclasses of the entity type recursively. The default EntityListBackingBean will be used if none are found.

Select Item Choices

Mapping a list of select Items to a ChoiceType is optional. If mapped, A ChoiceType can be used in place of its mapped Select Items. For example,

	public static final ChoiceType EMPLOYEE_TYPE = new ChoiceType("EMPLOYEE_TYPE");

	private static final NameValuePair[] employeeTypes = {
		new NameValuePair("FullTime",  Employee.Type.FULL_TIME),
		new NameValuePair("PartTime",  Employee.Type.PART_TIME)
	};

	@Override
	public Map<ChoiceType, NameValuePair[]> getSelectItemListMap() {
		HashMap<ChoiceType, NameValuePair[]> selectItemDataSourceMap = new HashMap<>();

		selectItemDataSourceMap.put(EMPLOYEE_TYPE, employeeTypes);
		return selectItemDataSourceMap;
	}
Then use SelectItemListProvider to get select items using Choice.
public class EmployeeBean extends EntityBackingBean<Employee> {
	@Override
	public List<SelectItem> getPropertySelectItems(
			EntityProperty<Employee> property) throws SystemException {
		ChoiceType choiceType;
		String propertyName = property.getName();
		if (propertyName.equals("type"))
			choiceType = HrModule.EMPLOYEE_TYPE;
		else
			return super.getPropertySelectItems(property);

		return SelectItemListProvider.getInstance().getSelectItems(choiceType, property);
	}
}
Refer to the javadoc for class SelectItemListProvider for more details.

Id Rules

For the entity types that extend Hierarchy or implement NormativeId, they have Id Rules that can be configured by users.

	@Override
	public List<Class> getIdRuleSupportEntityTypes(InstanceType instanceType) {
		return Arrays.asList(Employee.class, ExpenseClaim.class);
	}
The section Module Menu has an example of adding a menu node for managing Id Rules. Refer to the Id Rules of System module documentation for more details.

Form Designs

If an entity type allows user to customize its entity view(content and layout), then a FormDesignDescriptor instance needs to be registered. For example, user can create or select a Form Design(content and layout) for ExpenseClaim view if Form Design is enabled for ExpenseClaim type. A module can provide Form Designs that will be automatically available, and user can import and override them if needed.

For example, to enable Form Designs for ExpenseClaim entities:


	@Override
	public Map<Class, FormDesignDescriptor> getFormDesignDescriptorMap() {
		Map<Class, FormDesignDescriptor> formDesignDescriptorMap = new LinkedHashMap<>();

		// ExpenseClaim, use default FormDesign implementation
		formDesignDescriptorMap.put(ExpenseClaim.class, new FormDesignDescriptor());
		return formDesignDescriptorMap;
	}
The section Module Menu has an example of adding a menu node for managing Form Designs.

Embedded Objects

A module can create objects that are embeddable in a page. For example, embed a login into a page:
	<object xmlns="http://www.cmobilecom.com/af/objects" type="login">
		<viewConfig>
			<propertiesToShow>username,password</propertiesToShow>
		</viewConfig>
	</object>
Implement createEmbeddedObject(...) method to create embedded objects.

	@Override
	public BackingBean createEmbeddedObject(Component parentComponent, String objectType,
			Element objectElem, ContainerBean containerBean) throws SystemException {
	}
see Embedded Objects for more details.

Shortcut Menu Nodes

To add menu nodes into toolbar and add content to the home page of manage center, create shortcut menu nodes that refer to menu nodes of the module menu. For example, in the example HR module, add Employees in both toolbar and home page content to show the pageable list of all employees.

	@Override
	public List<MenuNode> getShortcutMenuNodes(MenuBean menuBean, ShortcutType type, 
			ShortcutMenuNodeFactory factory) throws SystemException {
		List<MenuNode> shortcutMenuNodes = new ArrayList<MenuNode>();

		// Employees: toolbar and homeContent
		MenuNode employeesShortcut = factory.createShortcutMenuNode(
				menuBean, null, IconMap.getIcon(IconMap.UI_ICON_PERSON), MODULE_HR,
				Employee.class, TypedMenuNodeFactory.COMMAND_SHOW_ENTITIES);
		shortcutMenuNodes.add(employeesShortcut);

		return shortcutMenuNodes;
	}
The shortcut type is either toolbar or home page content. To add a shortcut to toolbar only,

	if (type.equals(ShortcutType.TOOLBAR)) {
		MenuNode employeesShortcut = factory.createShortcutMenuNode(
				menuBean, null, IconMap.getIcon(IconMap.UI_ICON_PERSON), MODULE_HR,
				Employee.class, TypedMenuNodeFactory.COMMAND_SHOW_ENTITIES);
		shortcutMenuNodes.add(employeesShortcut);
	}
When showing the home page of manager center, the contents resulted from all the shortcut menu nodes will be displayed.

Access Control

Create access control XML file (ac.xml) for the module. see Access Control for details.

ORM mappings

Create JPA ORM mapping file (conf/orm.xml) for module managed types. For example,
<entity-mappings
		xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm cmobilecom-jpa-orm_1_0.xsd"
		version="2.2">

    <description>HR module: mapped entity types</description>
    <package>com.cmobilecom.af.example.hr.entity</package>
    
	<entity class="Address" />
	<entity class="Department" />
	<entity class="Employee" />
	<entity class="EmployeePhoto" />
	<entity class="ExpenseClaim" />
	<entity class="ExpenseClaimItem" />

</entity-mappings>
JPA annotations are required for entities and their properties. The ORM mapping file lists the managed types to avoid jar scanning at runtime.

And the conf/orm.xml file to the persistence units in META-INF/persistence.xml of the root project. For example,

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
				 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
				 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
				 version="2.0">

	<persistence-unit name="hr" transaction-type="RESOURCE_LOCAL">
		<provider>com.cmobilecom.jpa.spi.PersistenceProviderImpl</provider>

		
		<mapping-file>system/conf/orm.xml</mapping-file>
		<mapping-file>website/conf/orm.xml</mapping-file>
		<mapping-file>hr/conf/orm.xml</mapping-file>
		
		
		<exclude-unlisted-classes>true</exclude-unlisted-classes>
		<shared-cache-mode>NONE</shared-cache-mode>

		<properties>
			<property name="com.cmobilecom.jpa.PKasFK.onDeleteCascade" value="true" />
			<!-- Enable unique identifier verification for app-managed multitenancy. -->
			<property name="com.cmobilecom.jpa.multitenant.appManaged.supported" value="true" />

			<!-- properties to resolve expressions in ORM mappings -->
			<property name="multitenant.enabled" value="true"/>
			<!-- User: shared with system instance -->
			<property name="user.multitenant.enabled" value="false"/>
		</properties>
	</persistence-unit>

</persistence>
The file path for orm.xml in META-INF/persistence.xml is moduleName_lowercase/conf/orm.xml.

Register Persistence Entity Managers

A module needs to register extended PersistenceEntityManager(s) if any.

	@Override
	public void registerPersistenceEntityManagers() {
		PersistenceEntityManagerRegistry.register(persistenceEntityManagerSubtype, implClass);
	}
see Extend Persistence Entity Manager for details.

Database Seed SQL

In development phase, let Cmobilecom JPA generate DDL for creating database tables and constraints. For production, provide a seed.sql that will be run during installation or when an instance containing the module is created.

The default seed sql list consists of the seed.sql for the dbms type. For example,

	moduleRootDir/src/main/resources/db/
		mysql/seed.sql
		oracle/seed.sql
If seed sql is different for different instance types, separate the different part as a file. For example, foo.sql
	moduleRootDir/src/main/resources/db/
		mysql/seed.sql
		mysql/foo.sql		
		oracle/seed.sql
		oracle/foo.sql
Add a parameter(e.g., foo) to the InstanceType in conf/system-config.xl, and override the following method:
	@Override
	public List<String> getSeedSqlList(InstanceType instanceType) {
		List<String> seedSqlList = super.getSeedSqlList(instanceType);
		
		String foo = instanceType.getParameter("foo");
		if (foo != null)
			seedSqlList.add("foo.sql");
			
		return seedSqlList;
	}

Module Initialization

The seed sql(s) are used to create tables and constraints. But the module initialize() method should be used to create entities for seeding data. The module initialize() method will be called once when an instance(DataAccessUnit) is created. But the method must be implemented in the way that it can be called many times without any side effects.

The initialize() method of AbstractModule does the following:

A module can override initialize() to create more entities.

	@Override
	public void initialize(InstanceType instanceType, Instance instance) throws SystemException {
		super.initialize(instanceType, subsystem);
		...
	}

Module Implementation For Web

If there are code or resources especially for web, they need to be implemented or registered in the module implementation for web. For example,

// platform/device independent
package com.cmobilecom.af.examples.hr;
public class HrModule extends AbstractModule {

}

// for web
package com.cmobilecom.af.examples.hr.web;
public class WebHrModule extends HrModule implements WebModule {

}
Follow the naming conventions for packages and module implementation class names. The module implementation class com.cmobilecom.af.examples.hr.HrModule in conf/system-config.xml will be replaced by com.cmobilecom.af.examples.hr.web.WebHrModule at system startup for web container.

URL Rewriting For Web

Every page (website page or manage page) with embedded objects has a URL, which support default URL rewriting rules. For website pages, rewrite rules can be configured for each website. If a module needs to process URLs that are not supported by system, the module needs to implement the following two methods:

	@Override
	public boolean processRequestURL(HttpServletRequest request,
			ServletResponse response, String uriWithoutContextPath) throws SystemException, ServletException, IOException {
		// use RewriteRule engine, rewrite rules can be in XML.
		List<UrlRewriteRule> urlRewriteRules = UrlRewriteRuleParser.parse(xmlDocument);		

		return new UrlRewriteEngine(urlRewriteRules, null).processRequestURL(
				request, response, uriWithoutContextPath);
	}
	
	@Override
	public void processRequestParameters(ContainerBean containerBean) {
		...
	}
Refer to Website module documentations for URL rewriting rules.

Entity Export/Import

Refer to Chapter: Entity Export/Import.

Resource Bundles

Refer to Chapter: Resource Bundles.
Resource BundleEmbedded Objects: OverviewFrames / No Frames