Module Development

Cmobilecom AF is module pluggable. A module can be developed and added to an instance type (system or subsystem) in system-config.xml.

Module Interface

First of all, implement the Module interface. Usually extend AbstractModule and override some methods as needed. For the ExampleHr module,

public class HrModule extends AbstractModule {
	...
}

Module Name

Each module requires a unique name.

	@Override
	public String getName() {
		return "ExampleHR";
	}

License

By default, the license feature [moduleName].Module is required for the module to be fully functional.

	@Override
	public ModuleScopedName getLicenseFeatureRequired() {
		return new ModuleScopedName(getName(), "Module");
	}
If the module does not require a license or requires a different license feature, override the method getLicenseFeatureRequired(). For example,

	@Override
	public ModuleScopedName getLicenseFeatureRequired() {
		return null;  // license is not required
	}
If license is required for the module, 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 View Permission

By default, the permission [moduleName].ModuleView is required to access the module.

	@Override
	public ModuleScopedName getModuleViewPermission() {		
		return new ModuleScopedName(getName(), "ModuleView");
	}
If a permission is not required for users to access the module, or it requires a different permission, override getModuleViewPermission() method. For example,

	@Override
	public ModuleScopedName getModuleViewPermission() {
		return null;
	}
The permission name must exist in Permission table. Only the users that owns the permission can access the module.

Permissions

By overriding the following method, a module tells the framework about supported permissions. The permissions will be inserted into the Permission table when the module is initialized.
 
	@Override
	public List<String> getPermissions(InstanceType instanceType) {
	
	}
For example, ExampleHR module supports the following permissions.

	@Override
	public List<String> getPermissions(InstanceType instanceType) {
		return CollectionUtil.convertToList(new String[]{
				"ModuleAll",
				"ModuleView",
				"ModuleInit",
				
				"ManageADDR",
				"ManageDEPT",
				"ManageEMP",
				"ViewEMP",
				"ManageEC",
				"ViewEC",
				"ApproveEC"
		});
	}

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<Class, DataType>();

		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 this module. 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.

Persisted Entity Types

An entity type is mapped to a DataType, but it may not be persisted. Override the getMappedEntityTypes(instanceType) method to specify the list of entity types that will be persisted.

	@Override
	public List<Class> getMappedEntityTypes(InstanceType instanceType) {
		return CollectionUtil.convertToList(new Class[]{
				Employee.class, ExpenseClaim.class, ExpenseClaimItem.class
				});
	}
A module can define groups so that the module can be partially assembled into an InstanceType. see System Configuration. If an InstanceType does not specified groups for a module, the groups is null.

		List<Integer> groups = instanceType.getModuleGroups(getName());

Entity Type Names

Entity type names and its plural names can be specified by overriding the method getEntityTypeNameMap().

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

		typeNameMap.put(Employee.class, new String[]{"Employee", "Employees"});
		typeNameMap.put(ExpenseClaim.class, new String[]{"ExpenseClaim", "ExpenseClaims"}); 
		typeNameMap.put(ExpenseClaimItem.class, new String[]{"ExpenseClaimItem", "ExpenseClaimItems"}); 
		typeNameMap.put(ExpenseReportQueryForm.class, new String[]{"ExpenseReportQueryForm", "ExpenseReportQueryForm"}); 
		return typeNameMap;
	}
By default, an entity type name is using the name of @Entity annotation. The resource bundle for current locale will be used to translate entity type names for display.

Menu Node Factory

A module can provide a number of menu node factories. Factory names are specified in the InstanceType definitions of system configuration, and passed to the following createMenuNodeFactory(...) method.

 	<moduleNode module="ModuleFoo"  
   		menuNodeFactories="MenuNodeFactoryName1,MenuNodeFactoryName2">
 	</moduleNode>
If menuNodeFactories is not set, then factory name "default" will be used. Create one MenuNodeFactory instance for each factory name. For example,
  		
	@Override
	public MenuNodeFactory createMenuNodeFactory(MenuBean menuBean,
			MenuViewConfig viewConfig, ModuleNode moduleNode, String factoryName) {
		return new HrMenuNodeFactory(menuBean, viewConfig, moduleNode);
	}
The menu tree created by each MenuNodeFactory will be aggregated into the InstanceType menu.

The following ExampleHR example, its ModuleMenuNodeFactory creates a menu that includes Form Designs, Id Rules, Employees, ExpenseClaims and Expense Report.

 
public class HrMenuNodeFactory extends ModuleMenuNodeFactory {

	public HrMenuNodeFactory(MenuBean menuBean, MenuViewConfig viewConfig, ModuleNode moduleNode) {
		super(menuBean, viewConfig, moduleNode);    
	}
  
	@Override
	public MenuNode createMenuTree() throws BackingBeanException, SystemException {
		rootMenuNode = createModuleRootMenuNode("ExampleHR");		
		return rootMenuNode;
	}
	
	@Override
	protected void createSubMenu() throws SystemException {
		MenuViewConfig viewConfig = this.viewConfig.clone();

		MenuNode setupMenuNode = new MenuNode(this.menuBean, "HrSetup", "Setup", false);
		rootMenuNode.addChild(setupMenuNode);

		TypeDescriptor[] setupTypes = new TypeDescriptor[] {												
				getFormDesignTypeDescriptor(viewConfig),
				getIdRuleTypeDescriptor(viewConfig)
		};
		addTypedMenuNodes(this, setupMenuNode, setupTypes);		

		TypeDescriptor[] types = new TypeDescriptor[] {
				new TypeDescriptor<Employee>(Employee.class, null, viewConfig, null, true, null, null),
				new TypeDescriptor<ExpenseClaim>(ExpenseClaim.class, null, viewConfig, null, true, null, null)
				};
		addTypedMenuNodes(this, rootMenuNode, types);
		
		// report
		MenuNode reportMenuNode = new MenuNode(this.menuBean, "HrReport", "Report", false);
		rootMenuNode.addChild(reportMenuNode);
		
		TypeDescriptor[] reportTypes = new TypeDescriptor[] {
				new TypeDescriptor<ExpenseReportQueryForm>(ExpenseReportQueryForm.class, null, viewConfig, null, 
						true, null, null)
		};		
		addTypedMenuNodes(this, reportMenuNode, reportTypes);

	}
}	
TypedMenuNodeFactoryContext and CriteriaElement can be used to specialize a 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 Bean

Create the EntityBackingBean subclass for each entity type that users will interact with. User interaction includes creating, viewing, editing or query.
 	
	@Override
	public Map<Class<? extends PersistenceEntity>, Class<? extends EntityBackingBean>> getEntityBackingBeanMap() {
		Map<Class<? extends PersistenceEntity>, Class<? extends EntityBackingBean>> entityBackingBeanMap = 
			new HashMap<Class<? extends PersistenceEntity>, Class<? extends EntityBackingBean>>();

		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 a EntityBackingBean to be visible to users.

Entity List Backing Bean

EntityListBackingBean is the default backing bean for a list of entities, supporting in-place editing, pagination and row expansion. Most of its behavior 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 mapping 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<Class<? extends PersistenceEntity>, Class<? extends EntityListBackingBean>>();

		entityListBackingBeanMap.put(Employee.class, EmployeeListBean.class);
		return entityListBackingBeanMap;
	}

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,

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

		selectItemDataSourceMap.put(EMPLOYEE_TYPE, employeeTypes);
		selectItemDataSourceMap.put(EXPENSE_REPORT_DATA, expenseReportData);
		selectItemDataSourceMap.put(EXPENSE_REPORT_GROUP_BY, expenseReportGroupBy);
		return selectItemDataSourceMap;
	}
see the javadoc for class SelectItemListProvider API.

Id Rules

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

	@Override
	public List<Class> getIdRuleSupportEntityTypes(InstanceType instanceType) {
		return CollectionUtil.convertToList(new Class[]{Employee.class, ExpenseClaim.class});
	}

Form Design

If an entity type allows users to customize the layout of its properties, then a FormDesignDescriptor instance is required. For example, if permitted, users can design the layout of ExpenseClaim. When creating or showing an ExpenseClaim, a layout(Form Design) can be choosen.

	@Override
	public Map<Class, FormDesignDescriptor> getFormDesignDescriptorMap() {
		Map<Class, FormDesignDescriptor> formDesignViewConfigDescriptorMap = new LinkedHashMap<Class, FormDesignDescriptor>();
		
		// ExpenseClaim, use default FormDesign implementation
		ViewConfigDescriptor ecViewConfigDescriptor = new ViewConfigDescriptor(
			ExpenseClaim.PROPERTY_EXPENSE_CLAIM_ITEMS, ViewType.ENTITY_LIST_NARROW,
			null, true
		);
		formDesignViewConfigDescriptorMap.put(ExpenseClaim.class, 
			new FormDesignDescriptor(null, new ViewConfigDescriptor[]{ecViewConfigDescriptor}));
		
		return formDesignViewConfigDescriptorMap;
	}

Embedded Objects

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

	public BackingBean createEmbeddedObject(Component parentComponent, String objectType,
			Element objectElem, ContainerBean containerBean) throws SystemException;
see Embedded Objects.

Shortcut Menu Nodes

To add menu nodes into toolbar and home page content of manage center, create shortcut menu nodes that refer to the menu nodes of module menus. For example, in ExampleHR module, add employees in both toolbar and home page content.

	@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, MenuNode.UI_ICON_PERSON, MODULE_EXAMPLE_HR, 
				Employee.class, TypedMenuNodeFactory.COMMAND_SHOW_ALL_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, MenuNode.UI_ICON_PERSON, MODULE_EXAMPLE_HR, 
				Employee.class, TypedMenuNodeFactory.COMMAND_SHOW_ALL_ENTITIES);		
		shortcutMenuNodes.add(employeesShortcut);
	}
When showing 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.

Register Session Beans

see Persistence Entity Manager

ORM mapping

Create JPA ORM mapping file (orm.xml) for the module, and add it to the persistence units in META-INF/persistence.xml. For Java EE environment, module jar can be used instead.

JPA annotation is recommended for annotating entities and their properties. The ORM mapping file lists the mapped entity types only.

If the module supports groups, separate the orm.xml by groups. Add those ORM mappings that are applicable to persistence units in META-INF/persistence.xml.

Database Seed SQL

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

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

	moduleRootDir/src/main/resources/db/
		mysql/seed.sql
		oracle/seed.sql
If the module supports groups, separate the seed SQL by groups. For example, add foo.sql for group FOO.
	moduleRootDir/src/main/resources/db/
		mysql/seed.sql
		mysql/foo.sql		
		oracle/seed.sql
		oracle/foo.sql
Override the following method:
	@Override
	public List<String> getSeedSqlList(InstanceType instanceType) {
		List<String> seedSqlList = super.getSeedSqlList(instanceType);
		
		List<Integer> groups = instanceType.getModuleGroups(getName());
		if (groups == null || groups.contains(GROUP_FOO))
			seedSqlList.add("foo.sql");
			
		return seedSqlList;
	}

Initialize

The seed sql(s) are used to create tables and constraints. But to create entities, the module initialize() method should be used. The initialize() method will be called once after its instance (system or subsystem) is created. But the method must be implemented in the way that it can be called many times without any side effects.

The initialize() of AbstractModule does the following:

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

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

Module Impl 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
public class MyModule extends AbstractModule {
 

}

// for web
public class WebMyModule extends MyModule implements WebModule {
 

}
In the system-config.xml for web, the module class must be replaced with the one for web.

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) {
		...
	}
see the ExampleHR example and Bookmarkable URL. see the Website module documentations for URL rewriting rules.

Resources

See Module Resources for resource dependencies, bean resource encoding, and resource bundles.

Module Packaging

The ExampleHR module demonstrates the module directory structure and packaging using gradle build.

Directory structure:


moduleRootDir/
	src/main/java
		com/cmobilecom/af/example/hr
			entity
			model
			web
	src/main/resources
		conf/
			ac.xml
			orm.xml	
		db/
			[dbmsType]/seed.sql	
		help/
			[locale]
		import/
			[dataType]

		bundle/
			messages.properties
			messages_zh.properties
			
The source should have entity and model packages. If there is web specific code, there should be a web package to put web specific code. For example,

	com.mycompany.module.entity
	com.mycompany.module.model
	com.mycompany.module.web

Module jar:


	java classes
	help/[locale]/[module]/...
	[module]/conf/...
	[module]/db/...
	[module]/import/...
	[module]/bundle/...

More ...

Refer to the Module interface to implement more methods as needed.