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.
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 projectrootProject.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.
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.
@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";
}
@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;
}
@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.
@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.
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 |-- SearchMenuNodeFactory 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.
@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.
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.
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.
@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.
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.
<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.
@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.
<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.
@Override
public void registerPersistenceEntityManagers() {
PersistenceEntityManagerRegistry.register(persistenceEntityManagerSubtype, implClass);
}
see Extend Persistence Entity Manager for details.
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.sqlIf 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.sqlAdd 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; }
The initialize() method of AbstractModule does the following:
@Override
public void initialize(InstanceType instanceType, Instance instance) throws SystemException {
super.initialize(instanceType, subsystem);
...
}
// 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.
@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.