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 very 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.


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.



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


Minimal required set of software:


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.


Install using a package manager:


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


For project’s contributors, internal repo with branches:

git clone BGERP

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

git config "Shamil Vakhitov"
git config
git config pull.rebase true
git config core.fileMode false
git config core.longpaths true
git config core.autocrlf false

Read-only access

For only readers, including custom developers.

git clone BGERP


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/bgerp && docker run -d --name bgerp-demo --restart unless-stopped -p 3388:3306 bgerp/bgerp

Make sure, that the container is running using the command:

docker exec bgerp-demo /opt/bgerp/

Get configuration and data files from the container:

docker exec bgerp-demo cat /opt/bgerp/ | bash -c "sed 's#'" >
docker cp bgerp-demo:/opt/bgerp/
docker cp bgerp-demo:/opt/bgerp/filestorage filestorage
Use the command for removing the created container:
docker rm -f bgerp-demo
For accessing the DB instance with console SQL client use the command:
docker exec -it bgerp-demo mysql -ubgerp -p<PASSWORD_FROM_PROPERTIES> bgerp

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: ru.bgerp.Server

  • Program arguments: start

  • VM arguments:

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

Code formatting

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. Intention:

  • 4 whitespaces - Java;

  • 2 whitespaces - SQL scripts;

  • tabs - all the rest.

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.


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



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

Right-click on the project and invoke Refresh Gradle Project menu item. Also use it every time you change any of the project’s libraries.

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.


  • 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.


A taken from DB container file doesn’t produce output to STDOUT IDE console. In order to change that you have to add out appender there., file, session, out, file, session, out, 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.



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: 22 - 28

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 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 - 8930

controls = (!inst.inline ? 
                // PATCH ADDITION BGERP,  added X button, changed closeProcess
                '<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" onclick="$.datepicker._clearAndHideDatepicker(\'#' + + '\');">' + 
                "X" + '</button>' +
                '<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" onclick="$.datepicker._setNowIfEmptySaveAndHideDatepicker(\'#' + + '\');">' + 
                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: -L8081:Y.Y.Y.Y:8080

  • Create your own 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.


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:

diag d0b545adbd700daf7b25e599b723ed1d

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.


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.


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 ru.bgcrm.plugin.Plugin Class example; ru.bgerp.plugin.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.


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

// PzdcDoc snippet of: 'ru.bgcrm.plugin.slack.Plugin', lines: 30 - 32

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

In the particular case shown above, function handler gets included into JEXL context.


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

  object_type VARCHAR(100) NOT NULL,
  object_id INT NOT NULL,
  scheduled_dt DATETIME,
  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.


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: 90 - 96

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


        return json(con, form);


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.


Extension points definition within Plugin declaration:

// PzdcDoc snippet of: 'ru.bgerp.plugin.blow.Plugin', lines: 21 - 21

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: 'ru.bgerp.plugin.blow.Plugin', lines: 23 - 23

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


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>


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


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


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
















Test zones

For testing MVC framework available special hidden zones.

Interface Action path, mapping JSP Action class JSP path








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: 13 - 15

@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/ 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: 19 - 23

    public ActionForward unspecified(DynActionForm form, ConnectionSet conSet)
            throws Exception {
        return html(conSet, form, JSP_CUSTOM);

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

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

    public ActionForward runClass(DynActionForm form, ConnectionSet conSet)
        throws Exception {
        String className = form.getParam("class");

        String ifaceType = form.getParam("iface", "event");
        // running interface EventListener
        if ("event".equals(ifaceType))
            EventProcessor.processEvent(new RunClassRequestEvent(form), className, conSet);
        // running interface Runnable
        else {
            Class<?> clazz = null;
            try {
                clazz = Class.forName(className);
            } catch (ClassNotFoundException e) {
                throw new BGMessageException("Класс не найден: %s", className);

            if (Runnable.class.isAssignableFrom(clazz)) {
                boolean sync = form.getParamBoolean("sync");
                if (sync)
                    ((Runnable) clazz.getDeclaredConstructor().newInstance()).run();
                    new Thread((Runnable) clazz.getDeclaredConstructor().newInstance()).start();
            } else {
                throw new BGMessageException("Класс не реализует java.lang.Runnable: %s", 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: 'ru.bgerp.plugin.blow.action.BoardAction', lines: 40 - 60

public ActionForward show(ActionMapping mapping, 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.getUser()).getProcessList(boardConf);

        Set<Integer> processIds = -> p.getId()).collect(Collectors.toSet());

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

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

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

        updatePersonalization(form, con, persMap -> persMap.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.struts.action.DispatchAction', lines: 46 - 52

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

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

    return html(conSet, mapping, form, "messageList");
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.

Main identifier can be preceded by additional ones, separated by a comma - for example, when renaming classes or methods. This allows to provide backwards compatibility with permissions already present in the DB.

The actual main identifier stays always last and will be used upon the next save operation of the permission set. In the following example an action class was renamed and action method changed to unspecified, defined in the configuration by null.

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

<item title="App">
        <item action="ru.bgcrm.struts.action.admin.AppAction:status, ru.bgcrm.struts.action.admin.StateAction:null, org.bgerp.action.admin.AppAction:status" ltitle="Статус"/>

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 - 5 -->

<item action="org.bgerp.plugin.svc.backup.action.BackupAction:null" title="Backup">
        <item action="org.bgerp.plugin.svc.backup.action.BackupAction:backup" ltitle="Создать Backup"/>
        <item action="org.bgerp.plugin.svc.backup.action.BackupAction:downloadFileBackup" ltitle="Загрузить файл"/>

In the latest example notice also attribute ltitle, which means that the title goes through localization system.

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: 39 - 41

for (Process link : result)
    if (link.getCloseTime() == null)
        throw new BGMessageException("Есть незакрытый процесс: %s, привязанный к данному с типом: %s", 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: image::_res/project_jsp_schema.png[]


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


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.


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 test 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.


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.


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="ru.bgerp.plugin.blow.struts.action.BoardAction:search">
                        <form action="/user/plugin/blow/">
                                <input type="hidden" name="action" value="search"/>
                                <c:forEach var="id" items="${}">

For combining permission check with other checks use function p.check.

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/process/process/link_process_list.jsp', lines: 32 - 33 --%>

        <c:if test="${('1', false, '', 'processCreateLinkModeSelect') ne '0') and
                (p.check(ctxUser, 'ru.bgcrm.struts.action.ProcessAction:linkProcessCreate'))}">
shell:title and shell:state

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

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

<shell:title ltext="Конфигурация"/>
<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: 456 - 460

function openUrl(url, selectorStart) {

        openUrlPos(url, selectorStart, "last");

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.


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.

diag fa60097de6559303acd31dfacc9aab84


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.


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-authorization 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 -->

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

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>


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:

        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.

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


Localized key string may contain placeholders for some values, marked as {}. Such case may be seen for BGMessageException sample.


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.

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.


  • Database table and column names have to use underscore: process_id, param_id

  • 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: 422 - 422
    UNIQUE KEY id_param_id_n (`id`,`param_id`,`n`),

Java variables, HTTP request parameters, JSP variables

Have to use camelCase notation: processId paramId

Date - field entity

If a field has only date, then:

  • Java: createdDate type java.util.Date

  • DB: created_date type DATE

If a field has date + time, then:

  • Java: createdTime type java.util.Date

  • DB: created_dt type DATETIME

Date - period entity

Period entity must have two logically connected date or time fields, defining the same period. There is might be a fromDate field if it is not paired and and dateFrom that mandatory has dateTo pair.

If saving in bins, period has to use java.util.Date type with the following naming convention:

  • dateFrom

  • dateTo

Corresponding set and get methods:

  • setDateFrom

  • setDateTo

  • getDateFrom

  • getDateTo

Do not use Calendar beans! Use TimeUtils to convert into Calendar or from Calendar.

For storing 'time', use:

  • timeFrom

  • timeTo

    1. and also java.util.Date type.

Use Calendar for various calculators/billings when you need to constantly adjust dates.

Use date_from and date_to in the database (DATE type)

dt_from, dt_to - DATETIME type.

Infinity time or date - NULL.

Code samples

As system evolves and gets a lot of changes, this section contains referencing code examples. Snippets below gets extracted from an actual project code, so these are always reliable. Use IDE, if needed, in order to search for specific classes or files.



ru.bgerp.util.Log logger class is used, which is based on Log4j framework. When launching from IDE, you can copy build/bgerp/files/ 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: 'ru.bgcrm.worker.MessageExchange', lines: 17 - 20

public class MessageExchange extends ConfigurableTask {
    private static final AtomicBoolean run = new AtomicBoolean(false);

    private static final Log log = Log.getLog();

Log message with substitutions:

// PzdcDoc snippet of: 'ru.bgcrm.Server', lines: 115 - 115

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

Exception message output:

// PzdcDoc snippet of: 'ru.bgcrm.worker.MessageExchange', lines: 61 - 65

    try {
    } catch (Exception e) {


Show debug data instead of console.log:

// PzdcDoc snippet of: 'webapps/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: 18 - 22

$$.debugAreas = {
        openUrl: 0,
        ajax: 0,
        shell: 0,
        buffer: 0,

Saving latest/last user request parameters

Let’s take interface filters, as example - use restoreRequestParams method in ru.bgcrm.struts.action.BaseAction

Saving a value:

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

        restoreRequestParams(conSet.getConnection(), form, true, false, "messageTypeAdd");

Restoring a value:

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

public ActionForward messageUpdate(DynActionForm form, ConnectionSet conSet)
        throws Exception {
    var type = getType(form.getParamInt("typeId"));

    // сохранение типа сообщения, чтобы в следующий раз выбрать в редакторе его
    if (form.getId() <= 0) {
        form.setParam("messageTypeAdd", String.valueOf(type.getId()));
        restoreRequestParams(conSet.getConnection(), form, false, true, "messageTypeAdd");

Saving and restoring a value at once:

// PzdcDoc snippet of: 'ru.bgcrm.struts.action.ProcessLinkAction', lines: 53 - 57

// процессы, к которым привязана сущность
public ActionForward linkedProcessList(ActionMapping mapping, DynActionForm form, Connection con) throws Exception {
    ProcessLinkDAO processLinkDAO = new ProcessLinkDAO(con, form.getUser());

    restoreRequestParams(con, form, true, true, "open");

Feature flag for UI

Option stored in user personalization map. UI dropbox available in user profile settings.

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/profile/default.jsp', lines: 39 - 45 --%>

<c:set var="key" value="iface.buffer.openOnLongPress"/>
<ui:combo-single hiddenName="${key}" value="${ctxUser.personalizationMap.get(key, '')}" widthTextValue="50px">
        <jsp:attribute name="valuesHtml">
                <li value="0">${l.l('Нет')}</li>
                <li value="1">${l.l('Да')}</li>

Stored values may be read in JSP pages using in the same ctxUser.personalizationMap way, but also available in JS like for this case.

// PzdcDoc snippet of: 'webapps/js/', lines: 433 - 434

if (($$.pers["iface.buffer.openOnLongPress"] || 0) === 1)        {
        const debug = $$.debug("buffer");

Depicting element count on a tab

For example, showing associated processes count. Gets saved on the first call. Use ru.bgcrm.model.IfaceState class

Refreshing value:

// PzdcDoc snippet of: 'ru.bgcrm.struts.action.ProcessLinkAction', lines: 216 - 223

// проверка и обновление статуса вкладки, если нужно
if (Strings.isNotBlank(form.getParam(IfaceState.REQUEST_PARAM_IFACE_ID))) {
    IfaceState ifaceState = new IfaceState(form);
    IfaceState currentState = new IfaceState(Process.OBJECT_TYPE, id, form,
    new IfaceStateDAO(con).compareAndUpdateState(ifaceState, currentState, form);

Show in JSP:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/process/process/process_editor.jsp', lines: 86 - 100 --%>

        <c:if test="${'1', false, '', 'processShowProcessLinks') eq '1'}">
                <c:set var="ifaceId" value="link_process"/>
                <c:set var="ifaceState" value="${ifaceStateMap[ifaceId]}"/>

                <c:url var="url" value="/user/process/">
                        <c:param name="action" value="linkProcessList"/>
                        <c:param name="id" value="${}"/>
                        <c:param name="linkedReferenceName" value="linkedProcessList"/>
                        <c:param name="linkReferenceName" value="linkProcessList"/>
                        <c:param name="ifaceId" value="${ifaceId}"/>
                        <c:param name="ifaceState" value="${ifaceState.state}"/>

                $tabs.tabs( "add", "${url}", "${l.l('Связанные процессы')}${ifaceState.getFormattedState()}" );

Date and time format

When formatting date and time values in Java or JSP , use the following formats in order to keep unified approach and maintain independence from the current locale:

  • ymd - year, month, date;

  • ymdh - year, month, day, hour;

  • ymdhm - year, month, day, hour, minute;

  • ymdhms - year, month, day, hour, minute, second.

Java code uses ru.bgcrm.util.TimeUtils class for date formatting, this class also contains format constants.

The same functions may be used in JSP using tu prefix:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/parameter/edit.jsp', lines: 160 - 160 --%>

<input type="text" name="value" value="${tu.format(data.value, type)}" id="${focusFieldUiid}" ${changeAttrs} onclick="${getCommand}"/>

Configuration Java Beans

In order to speed up parsing and validation, use Java class objects inherited from ru.bgcrm.util.Config For example: ru.bgcrm.model.config.IsolationConfig. This configuration supports constructor with a validation flag, which allows to check syntax when saving.

Configuration bin can be also obtained in JSP:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/plugin/task/task_list.jsp', lines: 4 - 4 --%>

<c:set var="config" value="${ctxSetup.getConfig('ru.bgcrm.plugin.task.Config')}"/> 


Putting results into JSP and rendering a form;

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/process/process/link_process_list.jsp', lines: 91 - 99 --%>

<c:set var="uiid" value="${u:uiid()}"/>
<html:form action="/user/process/link" styleId="${uiid}">
        <div style="display: inline-block;" class="tt bold mt05 mb05">${l.l('К процессу привязаны')}:</div>

        <input type="hidden" name="action" value="linkProcessList"/>
        <input type="hidden" name="id" value="${}"/>

        <ui:page-control nextCommand="; $$.ajax.load(this.form, $('#${uiid}').parent())"/>

Java action:

// PzdcDoc snippet of: 'ru.bgcrm.struts.action.ProcessLinkAction', lines: 207 - 209

// привязанные к процессу процессы
SearchResult<Pair<String, Process>> searchResultLink = new SearchResult<Pair<String, Process>>(form);
processLinkDao.searchLinkProcessList(searchResultLink, id, open);

File upload

JSP page:

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

<c:set var="uploadFormId" value="${u:uiid()}"/>
<form id="${uploadFormId}" action="/admin/" method="POST" enctype="multipart/form-data" name="form">
        <input type="hidden" name="action" value="licenseUpload"/>
        <input type="file" name="file" style="visibility: hidden; display: none;"/>
        <button type="button" class="btn-grey w100p mt1" onclick="$$.ajax.triggerUpload('${uploadFormId}');">${l.l('Загрузить файл лицензии')}</button>
        $(function () {
                $$.ajax.upload('${uploadFormId}', 'lic-upload-iframe', function () {
                        $$.ajax.load('${form.requestUrl}', $$.shell.$content(this));


// PzdcDoc snippet of: 'src/ru/bgcrm/struts/action/admin/', lines: 128 - 135

public ActionForward licenseUpload(ActionMapping mapping, DynActionForm form, ConnectionSet conSet) throws Exception {
    var file = form.getFile();

    IOUtils.copy(file.getInputStream(), new FileOutputStream(License.FILE_NAME));

    return json(conSet, form);

UI Only

A simple dictionary with pagination, AJAX editor invocation: webapps/WEB-INF/jspf/admin/process/status/list.jsp

Sending AJAX for sending, exiting or restoring data in process type properties editor:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/admin/process/type/properties.jsp', lines: 187 - 192 --%>

<div class="mt1">
        <button type="button" class="btn-grey mr1" onclick="$$$('#${formUiid}')[0], {toPostNames: ['config', 'matrix']}).done(() => $$.ajax.load('${editUrl}', $('#${formUiid}').parent()));">OK</button>
        <button type="button" class="btn-grey mr1" onclick="$$.ajax.load('${editUrl}', $('#${formUiid}').parent())">${l.l('Восстановить')}</button>

        <button type="button" class="btn-white ml1" onclick="$$.ajax.load('${form.returnUrl}', $('#${formUiid}').parent())">${l.l('К списку типов')}</button>

Recursive include:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/admin/user/check_tree_item.jsp', lines: 48 - 50 --%>

<c:if test="${not empty node.children}">
        <jsp:include page="check_tree_item.jsp" />

Action execution result include:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/admin/user/user/update.jsp', lines: 165 - 170 --%>

<c:url var="url" value="/admin/">
        <c:param name="action" value="userGroupList" />
        <c:param name="id" value="${}" />
        <c:param name="objectType" value="user" />
<c:import url="${url}" />

Flex layout, using constants from Java classes (defined in process_search_constants.jsp), print button close to a field:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/search/search.jsp', lines: 77 - 91 --%>

<div style="display: flex;">
                <%@ include file="process_search_constants.jsp"%>
                <ui:combo-single hiddenName="mode" style="width: 100%;">
                        <jsp:attribute name="valuesHtml">
                                <li value="${MODE_USER_CREATED}">${l.l('Cозданные мной')}</li>
                                <li value="${MODE_USER_CLOSED}">${l.l('Закрытые мной')}</li>
                                <li value="${MODE_USER_STATUS_CHANGED}">${l.l('Статус изменён мной')}</li>
        <div class="pl05">
                <ui:button type="out" onclick="this.form.elements['searchBy'].value='userId'; $$.ajax.load(this.form, '#searchResult');"/>
For some reason a class, containing included constant must not contain config word in package path.

Snap-in refresh upon moving back to it:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/log/log.jsp', lines: 17 - 24 --%>

        $(function () {
                const $log = $('#${uiid}').parent();
                $'onShow', function () {
                        $$.ajax.load("/user/", $log);

Sending AJAX request and showing progress indicator on button during execution:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/admin/run/run.jsp', lines: 20 - 24 --%>

                <button class="btn-grey ml1 icon" type="button" onclick="
                        $$, {control: this})
                                .done(() => {
                                        alert(this.form.sync.checked ? '${l.l('Класс выполнен, проверьте логи')}' : '${l.l('Класс запущен в отдельном потоке,\\nвывод в логах.')}')
                                })"><i class="ti-control-play"></i></button>

The same but for $$.ajax.load function:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/user/process/queue/filter.jsp', lines: 915 - 915 --%>

<ui:button type="out" styleClass="out" onclick="const $form = $('${selectorForm}'); processQueueMarkFilledFilters($form); $$.ajax.load($form, $('#processQueueData'), {control: this});"/>

Restore form parameter values:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/admin/directory/parameter/group/update.jsp', lines: 8 - 10 --%>

<html:form action="admin/directory" styleClass="center500">
        <input type="hidden" name="action" value="parameterGroupUpdate"/>
        <html:hidden property="directoryId"/>