BGERP is quite a large and complex project. It was created by a team consisting of ordinary humans, who may be imperfect and make mistakes but try to learn continuously. This is a trial and error process, fortunately or not.

This article provides some recommendations collected during the learning process - these are accompanied by a relative small amount of code for exactly the same reason. Said that, you have to follow those recommendations using The Boy Scout Rule.

Always leave the campground cleaner than you found it.

While changing a block of code, it is important to try to decrease the usage of outdated approaches, methods and classes. Write a new code and keep it compliant with the latest recommendations, but at the same time try to get neighboring/adjacent code improved. During changes testing, code improvements will be tested as well. All in all, gradually, we will improve overall code quality. We plan to introduce a set of formal code quality metrics: number of compiler warnings, format checks, etc.

Languages

The project currently undergoes a migration process from Russian language to an international one (English). All the software documentation, including this article, and all log messages will be converted to English. Information shown to the end-user via interface has to be localized.

Project

Environment

The program can be developed on any Java-supported platform.

Windows

Minimal required set of software:

Shell

System of build and publish is written on Bash scripts and uses GNU utilities, you can use the following when using Windows:

  • WSL 2 - embedded in Windows virtual Linux machine;

  • Cygwin - Windows ports of GNU utils, not tested.

The best console emulator on Windows with embedded FAR Manager - ConEmu.

Linux

Install using a package manager:

Gradle

Gradle is a build tool, used for the project. It is running using gradlew wrapper script, provided in the sources.

To update a Gradle version in the wrapper, execute for example for 8.4:

./gradlew wrapper --gradle-version=8.4

Checkout

Checkout the project using GIT to an wanted directory, e.g.: BGERP.

Contributors

For project’s contributors, internal repo with branches:

git clone https://git.bgerp.org/bgerp/bgerp.git BGERP

Configure GIT inside the directory. Use your name and email instead.

git config user.name "Shamil Vakhitov"
git config user.email shamil@bgerp.org
git config pull.rebase true
git config core.fileMode false
git config core.longpaths true
git config core.autocrlf input

For Windows and Mac only.

git config core.ignorecase true

Read-only access

For only readers, including custom developers.

git clone https://github.com/Pingvin235/bgerp BGERP

Running

DB container

Use the Docker image for running developer DB instance. Run the following command inside of the project’s directory.

We expose non-standard MySQL port 3388 to avoid possible conflicts with a running locally MySQL server.
docker pull bgerp/devd && docker run -d --name bgerp-devd --restart unless-stopped -p 3388:3306 bgerp/devd && docker logs --follow bgerp-devd

Make sure, that the container is running, wait the message:

BGERP Dev DB is running

Get configuration and data files from the container:

docker exec bgerp-devd cat /opt/bgerp/bgerp.properties | bash -c "sed 's#127.0.0.1/#127.0.0.1:3388/#'" > bgerp.properties &&
docker cp bgerp-devd:/opt/bgerp/lic.data lic.data &&
docker cp bgerp-devd:/opt/bgerp/log4j.properties log4j.properties &&
docker cp bgerp-devd:/opt/bgerp/filestorage filestorage

Add the following values to the end the extracted bgerp.properties file:

# html title of web-app
title=BGERP DEMO LOCAL

# disable JS pooling sometimes is useful for development
#pooling.enable=0

# quick license check
test.license.check.notification=1
test.license.check.error=1

The running container has unused Java BGERP Server process with disabled Scheduler. If you will need to enable the Scheduler later, add appropriate parameter in bgerp.properties file as well.

Use the command for removing the created container:
docker rm -f bgerp-devd
For accessing the DB instance with console SQL client use the command:
docker exec -it bgerp-devd /opt/bgerp/mysql.sh

Command line

Execute command:

./gradlew startServer
The Gradle tasks ends on 75%.

After correct start of the server, Web interface has to be available at URL: http://localhost:9088/user Use admin - admin credentials.

Inside IDE

Configuration for running inside IDE:

  • Main class: org.bgerp.Server

  • Program arguments: start

  • VM arguments: -Dbgerp.setup.data=bgerp

  • Classpath: click on User entries - Advanced - Add folders and add project’s current folder.

Code formatting

Whitespaces

IDE or editor must be configured for displaying whitespace symbols.

ide format

Set trim trailing whitespaces on saving, the option is provided for VS Code Settings Template.

The projects mostly follow recommended formatting rules for each of programming languages they use. Indentions:

  • 4 whitespaces - Java, Gradle, Bash, YML;

  • tabs - SQL, XML, HTML, JS, JSP, Dockerfile.

However because of historical reasons many files have been formatted in a wrong way, and have to be carefully fixed. Screenshot above shows an example of a Java file still using tabs.

When working on fixing a file format, it is important to use the following rules:

  1. All new files should be formatted correctly.

  2. Avoid using both indent symbols (tabs and spaces) inside a single file! Such file becomes unreadable in certain editors. If you notice a file/case like this, make sure to change all indent symbols in the file to the required format.

  3. Be cautious when combining file formatting with making changes - this can significantly complicate analysis down the road.

Java

Jave code formatting should be Java Conventions-compliant, with the following additional changes (Eclipse formatter settings shown below)

Use settings in Windows - Preferences - Java - Code style - Formatter. Open a standard formatter and save it under a different name after changing the following parameters:

  • Indentation - Tab policy - Spaces only

  • Indentation - Tab size - 4

  • Line Wrapping - Maximum line width - 150

Window - Preferences - Java - Code style - Organize Imports - put 99 and 1 in the respective fields.

Example/resulting Eclipse formatter file: formatter.xml

SQL

All the SQL code has to be written with high case letters, except of names for tables, columns, variables.

SELECT id, title FROM user WHERE comment LIKE 'Admin';

IDE

Eclipse

Currently one of the most handy platforms. Download Eclipse for Java EE Developers, as this build already includes Gradle, JSP and XML editors. Install the following plugins:

Import the project directory as a Gradle Project.

Due to a bug in the Eclipse plugin define exact 6.9.1 version of Gradle before the import.

Window - Preferences - General - Editors - Text Editors - set a Show whitespace characters checkbox

Window - Preferences - Team - Git - History remove Relative history checkbox

Import Java formatter via Window - Preferences - Java - Codestyle - Formatter.

VS Code

A much faster than Eclipse, has better JavaScript and Gradle support.

Drawbacks:

  • JSP support is nearly absent;

  • external GIT client is required.

Install VS Code itself and the following extensions:

  • Java Extension Pack

  • GitLens

  • Git Graph

  • Git History

  • AsciiDoc

  • Eclipse Keymap - optionally

Visit open and navigate to .vscode catalog in project’s directory in order to get some configuration examples. Java formatter and space symbols indicators have been already enabled there.

Logging

A taken from DB container log4j.properties file doesn’t produce output to STDOUT IDE console. In order to change that you have to add out appender there.

log4j.logger.ru.bgcrm=ALL, file, session, out
log4j.logger.ru.bgerp=ALL, file, session, out
log4j.logger.org.bgerp=ALL, file, session, out

File’s structure

Project’s folder listing along with description.

  • bin - IDE-compiled Java classes;

  • build - files related to build;

  • docpattern - Document plugin templates;

  • filestorage - file storage when launched from IDE;

  • lib - Java libraries, which are not linked via Gradle;

  • plugin - plugin declarations;

  • src - Java source code;

  • srcx - documentation, unit-tests, integration tests, build utilities sources;

  • webapps - Web application’s root directory;

  • work - folder created by Tomcat for JSP file compilation.

Libraries

Java

When running a program, a number of JAR files are linked from the following folders:

  • lib/app - application’s and Custom classes;

  • lib/ext - external libraries.

External libraries are supplied with a separate update package. Artifacts and versions are indicated in the following file: build.gradle (for bgerp configuration)

This configuration does NOT use transitive dependencies - all required libraries and versions have to be explicitly defined. We would suggest to gradually add a small set of libraries until getting a "workable" application - this will allow to decrease build’s size and simplify further support and troubleshooting.

In order to speed up project initialization, our default Eclipse IDE configuration does not load source code and documentation.

// PzdcDoc snippet of: 'build.gradle', lines: 40 - 46

eclipse {
    classpath {
        // speed up of initial start, change if needed
        downloadJavadoc = false
        downloadSources = false
    }
}

After adding new libraries in build.gradle create an empty build/changes.lib.txt file using ./gradlew touchChangesLib command. Existence of the file causes build library update package during release build.

JS

JS libraries are located in the following folders:

  • webapps/js - application’s JS files: kernel’s and plugins';

  • webapps/lib - external libraries.

Non-minified libraries are used in order to simplify debugging.

Some of the external libraries have been patched and all changes are accompanied by relevant comments, for example:

// PzdcDoc snippet of: 'webapps/lib/jquery-ui-1.12.1/jquery-ui.js', lines: 8923 - 8929

controls = (!inst.inline ?
                // PATCH ADDITION BGERP,  added X button, changed closeProcess
                '<button type="button" class="ui-datepicker-close btn-white btn-small icon btn-close" onclick="$.datepicker._clearAndHideDatepicker(\'#' + inst.id + '\');"><i class="ti-close"></i></button>' +
                '<button type="button" class="ui-datepicker-close btn-white btn-small" onclick="$.datepicker._setNowIfEmptySaveAndHideDatepicker(\'#' + inst.id + '\');">' +
                this._get(inst, 'closeText') + '</button>'
                // END PATCH
                : '');

Remote development methodology

BGERP launches within IDE, gets connected to the database and, optionally, billing. At the end of the development/work session remote client database will have all the actual information, so it is only required to update the product itself.

  • Use SSH to connect to the client, utilize port-forwarding in order to connect to the database and, optionally, billing. Example: ssh user@X.X.X.X -L3307:127.0.0.1:3306 -L8081:Y.Y.Y.Y:8080

  • Create your own bgerp_customer.properties file - you can use it to keep your configuration and access parameters. This file does not get pushed to GIT. You can also use this file to alter billing connection URL - for example, to a localhost.

  • Reconfigure BGERP port so you could save passwords in your browser for specific client(s).

  • Create IDE launch configuration using this properties file

  • Optionally, alter .gitignore in order to save custom client’s files in case you are using fork.

Naming Conventions

Configuration variables, JS functions, HTTP StyleId

  • All plugin configuration variable names have to start with <plugin>:

  • All plugin’s JS function names have to start with <plugin>- prefix. "-" cannot be used within a function name in other places.

  • All DOM element identifiers (style id) for a plugin have to start with <plugin>- "-" cannot be used within a DOM element identifier name in other places.

Java and JSP variables, HTTP request parameters

Have to use camelCase notation: processId, paramId

SQL

Database table and column names have to be in lower case, underscore separated: user_group, process_id, param_id.

Date and time fields

Date only, day precision.

Language Type Naming for single date field per entity Naming for many date fields per entity Naming for period

Java

java.util.Date

date

createDate, closeDate, paymentDate, statusDate

dateFrom, dateTo

SQL

DATE

date

create_date, close_date, payment_date, status_date

date_from, date_to

Date plus time, second precision.

Language Type Naming for single datetime field per entity Naming for many datetime fields per entity Naming for period

Java

java.util.Date

time

createTime, closeTime, paymentTime, statusTime

timeFrom, timeTo

SQL

DATETIME

dt

create_dt, close_dt, payment_dt, status_dt

dt_from, dt_to

NULL means undefined datetime or plus/minus infinite value in period.

Architecture

BGERP is a standalone Java application with a dynamic Web-interface. When developing, you can launch it directly from IDE.

Figure below illustrates a request processing pipeline:

Request processing stages:

  1. JS is used to form a request via HTML form , which is then sent to a Java Action class method.

  2. Upon a change request system sends only a confirmation - JSON response with OK status

  3. Any exception on Java Action side triggers a JSON with an ERROR status to be sent towards a client side.

  4. Any successful data read request returns an HTML Fragment (Table with a list of usernames as an example), which can then be embedded into resulting HTML on the client side.

Below is a summary of a technology stack used, in descending order of importance:

  1. Java - all application logic is implemented as Java code, as Java provides best options in terms of easy of development, reliability and performance.

    1. MySQL + JDBC - data processing.

  2. HTML + CSS - browser’s markup language and style tables.

  3. JSP + JSTL - HTML page rendering templates, presentation layer;

  4. JS + JQuery - client-side scripting language for dynamic content, only used as predefined framework calls.

Pay attention to avoid mixing the layers, e.g. creating application logic outside of Java code, defining presentation layer without JSP, performing data manipulation without MySQL.

Plugin

Any logically detached functionality has to be moved into a plugin. Isolation is a primary trait of a plugin. Each plugin works with a kernel and kernel has no knowledge of the inner works of a given plugin. Plugins do not have to 'know' how other plugins work, either. 'Knowing' here means a necessity to rely on certain APIs or specific method calls.

Kernel

The special org.bgerp.plugin.kernel.Plugin plugin is responsible for keeping the kernel functionality of the system.

This plugin is a specific one, because:

  • always enabled and required for normal work of the program

  • because that do not need endpoints

  • Java classes spread outside a single PLUGIN_PACKAGE

  • JSP files are also in many directories

Using those assumptions the system may be presented as set of plugins.

Plugin’s Java class

Application detects plugin upon start by a mandatory Java class, extended from org.bgerp.plugin.pln.blow.Plugin Class example; org.bgerp.plugin.pln.blow.Plugin

Each plugin has a corresponding and unique:

  • PLUGIN_ID - a single and unique for a plugin English word, all single case (no upper/lower mix) and without special symbols.

  • Java PLUGIN_PACKAGE which includes this specific class.

Init

Plugin class init method is called for all enabled plugins during the server start.

// PzdcDoc snippet of: 'ru.bgcrm.plugin.slack.Plugin', lines: 37 - 39

EventProcessor.subscribe((e, conSet) -> {
    e.getContext().put(ID, new ExpressionObject());
}, ContextInitEvent.class);

In the particular case shown above, function handler gets included into JEXL context. First init is done for Kernel Plugin.

Database

If plugin uses DB, then plugin package can contain a script which creates or modifies tables. When db.sql script presented in PLUGIN_PACKAGE it gets invoked during standard initialization routine.

Below is an example of a script for a FullText plugin

// PzdcDoc snippet of: 'src/ru/bgcrm/plugin/fulltext/db.sql', lines: 1 - 11

CREATE TABLE IF NOT EXISTS fulltext_data (
        object_type VARCHAR(100) NOT NULL,
        object_id INT NOT NULL,
        scheduled_dt DATETIME,
        data TEXT NOT NULL,
        FULLTEXT (data) WITH PARSER ngram,
        KEY scheduled_dt (scheduled_dt),
        UNIQUE KEY type_id (object_type, object_id)
);

-- ! after must be only a new line !;

Plugin DB table names have to start from PLUGIN_ID.

DB Structure is documented in the common process, simultaneously with making changes.

Use PRIMARY key only for auto incremented INT columns, otherwise prefer named UNIQUE key.

// PzdcDoc snippet of: 'src/org/bgerp/plugin/kernel/db.sql', lines: 443 - 443

UNIQUE KEY id_param_id_n (`id`,`param_id`,`n`),
Database Queries Cache

To speed-up the application startup initialization all the already executed calls are cached in db_update_log table. The cache might be reset using installer console util.

To force re-execution a query, add a whitespace before ending semicolon on the query line.

// PzdcDoc snippet of: 'src/org/bgerp/plugin/kernel/db.sql', lines: 392 - 392

CALL drop_key_if_exists('param_address', 'PRIMARY');
Cache

Caching is used within internal Map and List in order to significantly (100s of times) expedite directory data retrieval when building UI or when running JEXL scripts Map allows to get specific value based on they kay (from a 'key-value' pair), list - an alphabetically sorted list. Users, User Groups, Process types, configurations are examples of such cached data.

Use caches as much as possible for getting reference values and avoid querying DB extensively.

Directory data is edited directly in DB and cache is cleared after an edit.

// PzdcDoc snippet of: 'ru.bgcrm.struts.action.admin.UserAction', lines: 89 - 95

    public ActionForward permsetDelete(DynActionForm form, Connection con) throws Exception {
        new UserPermsetDAO(con).deletePermset(form.getId());

        UserCache.flush(con);

        return json(con, form);
    }

Endpoints

Earlier plugin endpoints were described in plugin.xml file located within PLUGIN_PACKAGE

Endpoints used for connecting non-Java plugin parts:

  • JSP includes;

  • JS files.

JavaScript

Extension points definition within Plugin declaration:

// PzdcDoc snippet of: 'org.bgerp.plugin.pln.blow.Plugin', lines: 22 - 22

Endpoint.JS, List.of(Endpoint.getPathPluginJS(ID)),

File containing script: webapps/js/pl.blow.js

Menu items

Menu item in user interface.

Extension points definition within Plugin class:

// PzdcDoc snippet of: 'org.bgerp.plugin.pln.blow.Plugin', lines: 24 - 24

Endpoint.USER_PROCESS_MENU_ITEMS, List.of(PATH_JSP_USER + "/menu_items.jsp"),

Adding a menu item using JSP tag: webapps/WEB-INF/jspf/user/plugin/blow/menu_items.jsp

Action

Plugin actions classes, if used, have to be declared in PLUGIN_PACKAGE.action package.

action.xml with permission tree for user interface actions has to be located in PLUGIN_PACKAGE.

JSP templates:

  • user interface in webapps/WEB-INF/jspf/user/plugin/<PLUGIN_ID> or webapps/WEB-INF/jspf/admin/plugin/<PLUGIN_ID>

  • open interface in webapps/WEB-INF/jspf/open/plugin/<PLUGIN_ID>

Localization

Plugin’s localization file (i10n.xml) has to be located in PLUGIN_PACKAGE. It is only used for localizing actions and action’s JSP templates

MVC

The project is using Apache Struts framework in a very own and customized way:

  • requests are sent using AJAX and responses update HTML partially

  • from Struts JSP tags used only <html:form with <html:param inside

  • action methods have different signature as standard

  • the form object has always the same class

Interfaces

There are three user interfaces exist in the program.

The following table shows rules for path and packages of actions and JSP templates. Shortcuts there mean:

  • ppp - plugin ID;

  • aaa - action.

JSP paths are defined starting from webapps/WEB-INF directory.
Interface Action path Action class JSP path

user

/admin/aaa
/admin/plugin/ppp/aaa

…​action.admin.AaaAction
…​plugin.ppp.action.admin.AaaAction

jspf/admin/…​/aaa.jsp
jspf/admin/…​/plugin/ppp/…​/aaa.jsp

/user/aaa
/user/plugin/ppp/aaa

…​action.AaaAction
…​plugin.ppp.action.AaaAction

jspf/user/../aaa.jsp
jspf/user/../plugin/ppp/…​/aaa.jsp

usermob

/usermob/aaa

…​action.usermob.AaaAction

jspf/usermob/../aaa.jsp

open

/open/aaa
/open/plugin/ppp/aaa

…​action.open.AaaAction
…​plugin.ppp.action.open.AaaAction

jspf/open/../aaa.jsp
jspf/open/../plugin/ppp/…​/aaa.jsp

Demo Zones

For testing MVC framework available special demo zones, providing the actual set of UI components, same as examples of CRUD operations, permission check, etc. These zones can be used as playgrounds for developers, allowing to learn the system.

Interface Action path, mapping JSP Action class JSP path

user

/user/demo
jspf/user/menu.jsp

org.bgerp.action.DemoAction

jspf/user/demo.jsp

open

/open/demo
jspf/open/demo/url.jsp

org.bgerp.action.open.DemoAction

jspf/open/demo.jsp

demo zone

Java Action

Action definition in struts-confing.xml files is deprecated.

Action classes have to extend ru.bgcrm.struts.action.BaseAction class marked by Action annotation.

// PzdcDoc snippet of: 'org.bgerp.action.admin.RunAction', lines: 25 - 27

@Action(path = "/admin/run")
public class RunAction extends BaseAction {
    private static final String PATH_JSP = PATH_JSP_ADMIN + "/run";

The sample above means that HTTP requests with URL /admin/run.do mapped to the class. An action HTTP request’s parameter is used to indicate method’s name. If this HTTP parameter is not defined, then unspecified method gets invoked.

Action methods have to return by invoking html or json.

First method gets processed by JSP forward page and sends HTML back to the client.

// PzdcDoc snippet of: 'org.bgerp.action.admin.RunAction', lines: 32 - 36

    @Override
    public ActionForward unspecified(DynActionForm form, ConnectionSet conSet) throws Exception {
        form.setRequestAttribute("runnableClasses", runnableClasses());
        return html(conSet, form, PATH_JSP + "/run.jsp");
    }

Second one is used to perform changes and only returns JSON-based change execution result.

// PzdcDoc snippet of: 'org.bgerp.action.admin.RunAction', lines: 59 - 85

    public ActionForward runClass(DynActionForm form, ConnectionSet conSet) throws Exception {
        // 'class' is passed when value is chosen from drop-down, 'data' - entered directly
        String className = form.getParam("class");
        if (Utils.isBlankString(className))
            className = form.getParam("data");

        String ifaceType = form.getParam("iface", "event");
        // running interface EventListener
        if ("event".equals(ifaceType))
            ((EventListener<Event>) Bean.newInstance(className)).notify(new RunClassRequestEvent(form), conSet);
        // running interface Runnable
        else {
            Class<?> clazz = Bean.getClass(className);

            if (Runnable.class.isAssignableFrom(clazz)) {
                boolean sync = form.getParamBoolean("sync");
                if (sync)
                    ((Runnable) clazz.getDeclaredConstructor().newInstance()).run();
                else
                    new Thread((Runnable) clazz.getDeclaredConstructor().newInstance()).start();
            } else {
                throw new BGMessageException("The class does not implement java.lang.Runnable: {}", className);
            }
        }

        return json(conSet, form);
    }
Form Object

Each action class method call gets a form object ru.bgcrm.struts.form.DynActionForm as a parameter. This object contains a context of request’s execution;

  • User;

  • Request parameters and supplementary methods for parsing those parameters

Do not use a legacy action format with HttpClientRequest and HttpClientResponse parameters (note that they are still present in the form)

A snippet of an action method:

// PzdcDoc snippet of: 'org.bgerp.plugin.pln.blow.action.BoardAction', lines: 42 - 62

public ActionForward show(DynActionForm form, Connection con) throws Exception {
    BoardConfig boardConf = setup.getConfig(BoardsConfig.class).getBoard(form.getId());
    if (boardConf != null) {
        // первичные процессы
        List<Pair<Process, Map<String, Object>>> processes = new BoardDAO(con, form).getProcessList(boardConf);

        Set<Integer> processIds = processes.stream().map(Pair::getFirst).map(p -> p.getId()).collect(Collectors.toSet());

        // связи между процессами, пока используем только родительское отношение
        Collection<CommonObjectLink> links = new ProcessLinkDAO(con, form).getLinksOver(processIds);

        Board board = new Board(boardConf, processes, links);

        form.setResponseData("board", board);
        form.setResponseData("processIds", processIds);

        updatePersonalization(form, con, map -> map.put("blowBoardLastSelected", String.valueOf(form.getId())));
    }

    return html(con, form, PATH_JSP + "/show.jsp");
}

Result gets redirected to a JSP page: webapps/WEB-INF/jspf/user/plugin/blow/board/show.jsp.

Use the same form to pass data for JSP rendering, excluding some auxiliary directories. You can use form’s response field for this. When set to responseType=json, all data in the response gets serialized into JSON - that’s why it is important to put directories into HttpResponse.

// PzdcDoc snippet of: 'ru.bgcrm.plugin.dispatch.action.DispatchAction', lines: 48 - 54

public ActionForward messageList(DynActionForm form, ConnectionSet conSet) throws Exception {
    form.getHttpRequest().setAttribute("dispatchList", new DispatchDAO(conSet.getSlaveConnection()).dispatchList(null));

    new DispatchDAO(conSet.getConnection()).messageSearch(new Pageable<DispatchMessage>(form), form.getParamBoolean("sent", null));

    return html(conSet, form, PATH_JSP + "/message/list.jsp");
}
Naming Convention

Plugin’s action classes have to be placed into PLUGIN_PACKAGE.action package, class name should end with Action. Previously Action classes have been located within struts.action packages - this approach is considered obsolete.

user interface actions are divided into /user and /admin This separation will be used in future for distinguishing administrative calls.

usermob и open interfaces have their own actions - in this case package names and URL have to contain usermob и open, respectively. org.bgerp.action.usermob.ProcessAction provides an example of such action.

We recommend to use the following naming convention for methods:

  1. [optional] Name of the object being handled in case a given class works with several objects.

  2. Verb which defines method’s operation. For example, for CRUD these are: list, get, update, delete.

ru.bgcrm.struts.action.admin.UserAction has some example method names:

  • permsetList;

  • permsetGet;

  • permsetUpdate.

Another set of examples from ru.bgcrm.struts.action.admin.AppAction:

  • status - provides application status;

  • update - triggers update installation;

  • userLoggedList - provides a list of logged in users.

It is recommended to use identical names for both method and forward JSP files.

Permissions check

All action methods for user interface have to be defined within PLUGIN_PACKAGE/action.xml files. Examples:

Definitions from those files are forming a tree which is used for access control.

Each action is identified by a class AND method, separated by semicolon.

First primary action can be followed by additional ones, separated by a comma - for example, when renaming classes or methods. Enabling each action enables all the item. This allows to provide backwards compatibility with permissions already present in the DB. The first identifier is used upon the next save operation of the permission set.

In the following example an action class was renamed two times and action method changed first to unspecified, defined in the configuration by null and later to status.

<!-- PzdcDoc snippet of: 'org/bgerp/plugin/kernel/action.xml', lines: 143 - 144 -->

<item title="App">
        <item action="org.bgerp.action.admin.AppAction:status, ru.bgcrm.struts.action.admin.StateAction:null, ru.bgcrm.struts.action.admin.AppAction:status" title="Status"/>

The same principe may be used for grouping many methods to a single logical action, for example typical get and update calls.

<!-- PzdcDoc snippet of: 'org/bgerp/plugin/bil/invoice/action.xml', lines: 6 - 6 -->

<item action="org.bgerp.plugin.bil.invoice.action.InvoiceAction:get, org.bgerp.plugin.bil.invoice.action.InvoiceAction:update, org.bgerp.plugin.bil.billing.invoice.action.InvoiceAction:get, org.bgerp.plugin.bil.billing.invoice.action.InvoiceAction:update" title="Update"/>

Actions may be hierarchically organized when some parent action is required for accessing children.

<!-- PzdcDoc snippet of: 'org/bgerp/plugin/svc/backup/action.xml', lines: 2 - 7 -->

<item action="org.bgerp.plugin.svc.backup.action.admin.BackupAction:null" title="Backup">
        <item action="org.bgerp.plugin.svc.backup.action.admin.BackupAction:backup" title="Create"/>
        <item action="org.bgerp.plugin.svc.backup.action.admin.BackupAction:restore" title="Restore To"/>
        <item action="org.bgerp.plugin.svc.backup.action.admin.BackupAction:downloadFileBackup" title="Download"/>
        <item action="org.bgerp.plugin.svc.backup.action.admin.BackupAction:deleteFileBackup" title="Delete"/>
</item>

Auxillary actions, which have to be permanently allowed, have to be marked with allowAll="1" attribute.

Exception handling

Exception interrupts actions execution, also rolls back DB transaction - response will be always sent back as JSON. All Exception handling is defined centrally in ru.bgcrm.struts.action.BaseAction, DAO methods or script handlers just need to raise a given exception. Said that, action methods declarations and DAO just need to use throws java.lang.Exception.

ru.bgcrm.model.BGException class is typically used for system-generated exceptions and is rarely used elsewhere.

ru.bgcrm.model.BGMessageException which inherits from ru.bgcrm.model.BGException, is used to sent a localized message to the user, without writing this message in the log. For example:

// PzdcDoc snippet of: 'ru.bgcrm.event.listener.ProcessClosingListener', lines: 40 - 42

for (Process link : result)
    if (link.getCloseTime() == null)
        throw new BGMessageException("Есть незакрытый процесс: {}, привязанный к данному с типом: {}", link.getId(), linkType);
DB Operations

DB-related operations are performed via separate Java DAO (Data Access Objects) classes which are then used within actions. Actions are using transactional request processing: transaction starts before method gets invoked, then a) gets committed (COMMIT) if there are no errors when returning results or b) gets rolled back (ROLLBACK) if exception is thrown.

DB connection details have to be specified via action methods' con parameters. Some methods use conSet (ru.bgcrm.util.sql.ConnectionSet) parameter, which invoke DB connection via a separate call. The latter is more suitable for methods which do not demand a DB connection, or, on the contrary, for the ones demanding several connection types: to replica or "trash" DB for non-critical data (currently not supported)

Table names have to be specified via constants in order to improve code cohesion (see ru.bgcrm.dao.Tables as example). If a given table is only used in a single DAO class, then this constant has to be defined as private (ru.bgcrm.plugin.fulltext.dao.SearchDAO as example)

Use java.sql.PreparedStatement class for building queries or. alternatively, use its wrapper - ru.bgcrm.util.sql.PreparedDelay[]. The latter allows to 'glue' both queries and parameters and does not require to specify parameters' positions.

JSP View

HTML gets rendered on server-side using JSP templates after receiving data from Action. UI uses a set of unified components - thanks to that in most cases there is no need to install additional styles or create additional JS handlers. JSP UI code examples.

The principal schema of JSP work looks like:

project jsp schema
SetRequestParamsFilter

The filter is executing after action and sets in request object Java objects, those methods may be called. The following objects prefixes are available:

As an example see date and time format.

Beside of the mentioned static functions, the filter sets also caches. All the request parameters are set in class ru.bgcrm.servlet.filter.SetRequestParamsFilter

Functions

JSP functions are defined in the directory webapps/WEB-INF/tld. Same as for tags IDEs support autocompletion for them.

Most of the functions there are deprecated because of possibility to call Java analogs.

Using semicolon-separated JSP functions makes sense only for JSP specific things, like in the following cases.

Element IDs

HTML DOM model assumes all element IDs are globally defined using class and id attributes. Due to that it is quite tricky to track their usage and ensure their uniqueness. Moreover, developer has to create a single-page application, which prevents to keep state for hidden elements.

In order to overcome this limitation, project attaches JS handlers via HTML onClick (and the like) attributes using context variable 'this'. Another method used for that is using u:uiid() function to generate a unique identifier, which is then used in auto-generated JS call.

In webapps/WEB-INF/jspf/user/log/log.jsp might be seen how to completely avoid absolute element IDs.

Tags

Components are defined within JSP tags and are declared in webapps/WEB-INF/tags IDE Eclipse supports autocomplete when using them in the code.

ide jsp tag

UI Demo Zones provide several examples of using JSP tags with user control elements.

For kernel, webapps/WEB-INF/jspf/user/menu.jsp file defines menu items which are used to match URL and Java Action. For plugins this is done via extension points.

u:sc

JSP page stores all variables ones. Combined with includes and long templates, this can cause certain inconvenience. <u:sc> context recovery tag is used to overcome this problem - all variables defined with this tag are cleared upon exit.

p:check

The tag enables code inside it only when mentioned action is allowed for the current user.

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/plugin/blow/board/show.jsp', lines: 11 - 14 --%>

                <p:check action="org.bgerp.plugin.pln.blow.action.BoardAction:search">
                        <form action="${form.httpRequestURI}">
                                <input type="hidden" name="action" value="search"/>
                                <c:forEach var="id" items="${frd.processIds}">

For combining permission check with other checks use function ctxUser.checkPerm, pointing to ru.bgcrm.model.user.User.

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/message/process_message_list.jsp', lines: 210 - 210 --%>

<c:if test="${messageType.readable and message.read and ctxUser.checkPerm('ru.bgcrm.struts.action.MessageAction:messageUpdateRead')}">
shell:title and shell:state

Set the left area and the right areas of top line in user interface. Tag shell:state may create a documentation help link.

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/admin/config/list.jsp', lines: 38 - 39 --%>

<shell:title text="${l.l('Configuration')}"/>
<shell:state help="kernel/setup.html#config"/>

Java Script

JS is used to enabled dynamic content on the client side. Scripts and associated libraries are located within this path. plugins can have their own scripts.

In certain cases JS gets dynamically generated via JSP templates on a server-side. This method is generally discouraged as it significantly complicates broweser-side debugging.

System functions are organized as object hierarchy built from the $$ root object. For example: $$.ajax, $$.ui A typical scheme for attaching JS code to HTML is to setup event handler with a function call - AJAX example

Deprecated JS functions are marked using the following method:

// PzdcDoc snippet of: 'webapps/js/kernel.ajax.js', lines: 586 - 590

function formUrl(forms, excludeParams) {
        console.warn($$.deprecated);

        return $$.ajax.formUrl(forms, excludeParams);
}

When such a function gets invoked, browser console gets a "clickable" "Deprecated" message which allows to find a this deprecated function call It is forbidden to use those functions in the new code, usage has to be decreased in the existing code over time.

Customization

One of the main solution’s prioritized features are extensibility and flexibility. Said that, standard functions can be extended by using several methods.

However, it is important to understand that the most effective way to efficiently develop and maintain functionality longer-term is to either use built-in plugin system or include as part of the kernel and at the same time keep configurable parameters to a minimum. JEXL scripts or dynamic code can be used for a quick prototyping or in order to implement certain application logic which is extremely specific to a given installation.

Our experience shows that all such customizations over time crystallize successful solutions suitable for a much larger group of users. Such customizations have to be moved into a main code to make them part of the system and to then allow further community development and improvement.

The following diagram illustrates above-mentioned transformation dynamics for a code of different types. Size of a given rectangle correlates with a code size for a specific type. Code size is a cumulative value for all Customers - larger size from scattered customizations gets translated into a much smaller universal code within a single product.

Localization

All log messages are produced in English only.

Localization gets applied to a user interface and messages addresses to a system user. System language is defined globally within configuration.

Files

XML localization file l10n.xml are located in plugin’s packages. New localization phrases should be added to the end of the list in the file.

Custom localization may overwrite all of them and read out custom/l10n.xml file in case of existing that.

The key of the localizing phrase is the first entry in any language, for example Russian:

<p><ru>Требуется повторный вход</ru><en>Re-login is required</en></p>

SYS language

It is possible to use short abbreviated keys, representing them as records in a special system language, for example:

<!-- PzdcDoc snippet of: 'src/org/bgerp/plugin/msg/email/l10n.xml', lines: 3 - 9 -->

        <p>
                <sys>email.sign.standard</sys>
                <ru>Сообщение подготовлено системой BGERP (https://bgerp.ru).
Не изменяйте, пожалуйста, тему и не цитируйте данное сообщение в ответе!</ru>
                <en>The message was prepared by BGERP system (https://bgerp.org).
Please, do not change response subject and to not citate the message there!</en>
        </p>

In the example also might be seen how to use multiline phrases.

Own language

To change the labels on the buttons in the interface, add a separate localization (you cannot change the Russian localization, since it is used as a key), for example, change the button in the creation wizard, through the my language:

<p><ru>Завершить</ru><en>Finish</en><my>Создать заявку</my></p>

Code

Localization can be done in JSP templates and Java actions. In JS code, localization is available only if it is generated by JSP. In the JSP template code, the localization call from the example above looks like this:

$('#loginForm').dialog({
        modal: true,
        draggable: false,
        resizable: false,
        title: "${l.l('Требуется повторный вход')}",
        position: { my: "center top", at: "center top+100px", of: window }
});

By doing Java Action in object l the localization context is passed, containing phrases for the core and the plugin being called. Localized key string may contain placeholders for some values, marked as {}.

// PzdcDoc snippet of: 'ru.bgcrm.struts.action.MessageAction', lines: 235 - 235

message.setText(l.l("Перенесено из процесса #{}", process.getId()) + "\n\n" + message.getText());

BGMessageException can contain localized message, translated when shown to a user. In the following snippet might be seen, how it can be thrown in a plugin’s logic.

// PzdcDoc snippet of: 'org.bgerp.plugin.msg.email.Addresses', lines: 103 - 107

if (silent) {
    log.debug("Incorrect prefix: {}", prefix);
    continue;
} else
    throw new BGMessageException(new Localizer(lang, Plugin.INSTANCE.getLocalization()), "Incorrect prefix: {}", prefix);

As there is no way to make localization on the Frontend, phrases may be sent there from the Backend. Here are two samples for such cases.

// PzdcDoc snippet of: 'org.bgerp.app.dist.lic.License', lines: 153 - 156

form.getResponse().addEvent(
    new LicenseEvent(new Message(form.l.l("License Will Expire Soon"),
        form.l.l("Your license will expire at {}", TimeUtils.format(dateTo, TimeUtils.FORMAT_TYPE_YMD))),
        true));
// PzdcDoc snippet of: 'org.bgerp.cache.UserNewsCache', lines: 98 - 102

final var l = Localization.getLocalizer();

result = new NewsInfoEvent(searchResult.getList().size(), currentUnprocessedMessages, popupNews, blinkNews, blinkMessages);
result.message(l, "News");
result.message(l, "Unprocessed messages");

Development

All the new code has to be written using localized strings. For legacy code recommended during other changes replace static strings to localized.

For missing keys the following messages appear in log output:

01-02/00:44:01  WARN [http-nio-9088-exec-2] Localizer - Missing translation for pattern: 'Иниациализировать плагины'

After adding missing patterns, application server has to be re-started.

Logging

Java

ru.bgerp.util.Log logger class is used, which is based on Log4j framework. When launching from IDE, you can copy build/bgerp/files/log4j.properties file from distribution kit into project’s root folder and adapt, as needed.

Use log protected variable when logging within actions

Create static final class variable within Java classes:

// PzdcDoc snippet of: 'org.bgerp.exec.MessageExchange', lines: 18 - 19

public class MessageExchange extends Task {
    private static final Log log = Log.getLog();

Log message with substitutions:

// PzdcDoc snippet of: 'org.bgerp.Server', lines: 135 - 135

log.debug("Scan type: {}, name: {} => {}", type, name, result);

Exception message output:

// PzdcDoc snippet of: 'org.bgerp.exec.MessageExchange', lines: 46 - 50

try {
    type.process();
} catch (Exception e) {
    log.error(e);
}

JS

Show debug data instead of console.log:

// PzdcDoc snippet of: 'webapps/js/kernel.shell.js', lines: 5 - 12

        const debug = $$.debug("shell");

        const pushHistoryState = function (href) {
                if (!history.state || history.state.href != href) {
                        debug("pushHistoryState: ", href, "; current: ", history.state);
                        history.pushState({href: href}, null, href);
                }
        }

Enabling debug:

// PzdcDoc snippet of: 'webapps/js/kernel.js', lines: 19 - 23

$$.debugAreas = {
        openUrl: 0,
        ajax: 0,
        shell: 0,
        "shell.login": 0,