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.

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;

  • Linux;

  • MacOS.

Minimal required set of software:

Shell for Windows

Skip this chapter when using *NIX OS.

Console client for version control system GIT.

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.

Checkout

Checkout the project using GIT to an wanted directory, e.g.: 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 false

Running

DB container

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

docker pull bgerp/bgerp && docker run -p 3306:3306 --name bgerp-demo -d bgerp/bgerp

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

docker exec bgerp-demo /opt/bgerp/erp_status.sh

Get configuration and data files from the container:

docker cp bgerp-demo:/opt/bgerp/bgerp.properties bgerp.properties
docker cp bgerp-demo:/opt/bgerp/log4j.properties log4j.properties
docker cp bgerp-demo:/opt/bgerp/filestorage filestorage
Use the command for removing the created container:
docker rm -f bgerp-demo

Command line

Execute command:

gradlew startServer

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: -Dbgerp.setup.data=bgerp

  • 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

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.

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

IDE

Eclipse

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.

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

By default, a running in IDE application uses logging configuration from src/log4j.properties. In order to adjust it, copy the file to the project directory (will not be handled by GIT) and make required changes.

File’s structure

Project’s folder listing along with description.

Libraries

Java

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

External libraries are supplied with a separate build update. 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: 18 - 24

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

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 - 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(\'#' + inst.id + '\');">' + 
                "X" + '</button>' +
                '<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" onclick="$.datepicker._setNowIfEmptySaveAndHideDatepicker(\'#' + inst.id + '\');">' + 
                this._get(inst, 'closeText') + '</button>'
                // END PATCH
                : '');

Client’s development methodology

BGERP launches within IDE, gets connected to the database and, optinally, 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.

GIT Workflow

Table below describes GIT branches - standard GIT Workflow is used here. Same workflow is used, for example, for Linux kernel development.

Main idea is that merging is only happening 'downstream', i.e. from the main branch to secondary branches, in order to get to the actual state. Main branch has linear structure as all changes are incorporated via 'patches'. Secondary branches can be then deleted, as all the aggregated information from secondary branches will be present in the main branch.

Branch Based On Purpose CI

master

Stable release.

p<Process ID>-short-description

master

Release and documentation changes.

p11862-documentation

master

Documentation changes. Long-live branch, periodically gets incorporated into main.

documentation-change-request

p11862-documentation

Documentation changes proposals, can be merged back to p11862-documentation.

master is a main project branch and is used for builds.

  • Each change should be based on the process within BGERP, which is used for all information exchange for a given change.

  • Change ID is a p<Process ID> string, for example p13455

  • For each change create a separate GIT branch (based on master). Branch’s name should start from Change ID, then use "-" as delimiters. Example: p11788-link-filter-title

  • When developing, you can use any commit strategy within a branch: checkpoint commits, reverts/resets, commits to a new branch, etc. We would recommend to push intermediate commits, using GIT repository as a backup copy.

  • Main branch can be periodically merged to this "change" branch in order to synchronize with the actual state. This HAS to be done before the 'merge --squash' to master

  • All the necessary documentation changes are done simultaneously with the code changes

  • Client’s test build update and accompanying documentation can be built and published from the branch.

  • Branch has to add build/change.<Process ID>.txt file - if absent, it gets added automatically during update build This file has to describe new functions, fixes or other changes - each item on a separate row. Format is identical to changes.txt, which accumulates all build/change.<Process ID>.txt information when update gets published.

  • Request branch merge --squash to master once client-side development and testing completes for a given change by moving BGERP process into Acceptance status.

  • Process gets closed after check/acceptance, and change gets merged to master as a single commit with a comment starting with the change ID.

  • "Change" branch can be then deleted. Change author can be later identified by the change ID used in the comment.

One cannot test and use in parallel several changes within a single system. Each change will overwrite the previous one when update gets installed. In principle it is allowed to use multiple build branches, which are used as targets for multiple change branches' merges.

GIT Commands

Clean all links to non-existent/deleted branches:

git remote prune origin

Fetch master branch and update remote-tracking branch origin/master:

git fetch origin master:master

Acceptance and merge --squash

When change is accepted, 'change' branch is compared with the master - so merge master to the 'change' branch before committing a change.

branch compare

It is also important to create and populate a file listing all changes. You can use the following command for this:

bash -c "./gradlew touchChanges"

GIT commands to be used for merging change from pXXXXX-short-change-description branch to master:

git checkout pXXXXX-short-change-description && git pull
git commit --allow-empty -m "MERGED" && git push
git checkout master && git pull
git merge --squash pXXXXX-short-change-description

Commit with current GIT user.

git commit -am "pXXXXX Some change description."

Or for preserving the author in GitHub. Mapping internal to external mails is available in file .gitlab-ci-publish-author.sh

git commit -am "pXXXXX Some change description." --author="Developer Name <internal@gitlab.email>"

And finally, push.

git push

Update is published periodically from the main branch.

Long-lived branches

Long-lived branches only get changes which are not altering product functionality, for example: localization and documentation fixes, code formatting, tests. BGERP process for a change does not get closed , and merge --squash is performed multiple times upon completion of specific change(s)

Documentation

A particular case of long-lived branch is documentation. The source AsciiDoctor and resource files are placed in srcx/doc directory. Publication of the product manual runs automcatically from branch p11862-documentation.

Documentation might be built also locally from any branch and together with a change update package.

Documentation changes are recommended to be done at the end of branch development, using change file as an intermediate notes. The samples of documentation’s format may be found here.

Due the strict references and snippets checking, it is quite possible to have broken state of documentation even without .adoc files. Something like the following:

2020-05-25 12:17:39,149 INFO DocGenerator [main] Processing: srcx/doc/project.adoc
2020-05-25 12:17:39,844 ERROR Snippet [main] Snippet '../../src/ru/bgcrm/struts/action/MessageAction.java' doesn't start from: 'message.se', line number: 205, content: newProcess.setDescription(message.getSubject());
2020-05-25 12:17:39,855 ERROR Snippet [main] Snippet '../../src/ru/bgcrm/struts/action/MessageAction.java' doesn't end on: ');', line number: 71, content: if (message == null)
2020-05-25 12:17:39,859 ERROR Snippet [main] Snippet '../../src/ru/bgcrm/struts/action/MessageAction.java' doesn't start from: 'pu', line number: 241, content:
2020-05-25 12:17:39,860 ERROR Snippet [main] Snippet '../../src/ru/bgcrm/struts/action/MessageAction.java' doesn't end on: '}', line number: 253, content:
2020-05-25 12:17:39,911 ERROR Snippet [main] Snippet '../../webapps/WEB-INF/jspf/user/search/search.jsp' doesn't start from: '<div', line number: 1, content: <%@ page contentType="text/html; charset=UTF-8"%>
2020-05-25 12:17:39,911 ERROR Snippet [main] Snippet '../../webapps/WEB-INF/jspf/user/search/search.jsp' doesn't end on: '/div>', line number: 134, content: <%@ include file="/WEB-INF/jspf/shell_title.jsp"%>
2

For such cases here is the fixing algorithm. First, find the failing line in .adoc file:

snippet fix 1

After that, using branch comparison, find the new rows and change them in the .adoc:

snippet fix 2

Hints:

  • If documentation was already corrected in the current branch, you can create a mock branch on the last working state.

  • Use line numbers for searching over failing snippets.

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:

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.

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

Init

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

// PzdcDoc snippet of: 'ru.bgcrm.plugin.slack.Plugin', lines: 22 - 24

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

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

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.

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: 92 - 98

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

        UserCache.flush(con);

        return status(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: 'ru.bgerp.plugin.blow.Plugin', lines: 21 - 21

Endpoint.JS, 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_MENU_ITEMS, 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 have 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

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: 14 - 16

@Action(path = "/admin/run")
public class RunAction extends BaseAction {
    private static final String JSP_PATH = 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 data or status.

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

// PzdcDoc snippet of: 'org.bgerp.action.admin.RunAction', lines: 20 - 24

    @Override
    protected ActionForward unspecified(ActionMapping mapping, DynActionForm form, ConnectionSet conSet)
            throws Exception {
        return data(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: 26 - 55

    public ActionForward runClass(ActionMapping mapping, 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();
                else
                    new Thread((Runnable) clazz.getDeclaredConstructor().newInstance()).start();
            } else {
                throw new BGMessageException("Класс не реализует java.lang.Runnable: %s", className);
            }
        }

        return status(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 = processes.stream().map(Pair::getFirst).map(p -> 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 data(con, form, JSP_PATH + "/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 data(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="Приложение">
        <item action="ru.bgcrm.struts.action.admin.AppAction:status, ru.bgcrm.struts.action.admin.StateAction:null" title="Статус"/>

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.

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.

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

webapps/test.jsp provides several examples of using JSP tags with user control elements. Navigate to http://<host>:<port>/test.jsp in order to invoke template execution, alternatively visit a Demo server: https://demo.bgerp.org/test.jsp

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

function openUrl( url, selectorStart )
{
        console.warn($$.deprecated);

        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.

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.

diag fa60097de6559303acd31dfacc9aab84

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-authorization is required</en></p>

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

<p><sys>reauth.message</sys><ru>Требуется повторная авторизация</ru><en>Re-authorization is required</en></p>

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.

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

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

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

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.

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

1) Database and field names have to use underscore: process_id param_id

Java variables, HTTP request parameters, JSP variables

Have to use camelCase notation: processId paramId

Date - field entity

If a field has only date, then: createDate - Java - java.util.Date type create_date - DB - date type

If a field has date + time, then: createTime - Java - java.util.Date type create_dt - DB - datetime type

Date - period entity

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 bins! 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 from_date and to_date in the database ('date' type)

from_dt, to_dt - datetime type.

Infinity time/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.

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: 'ru.bgcrm.worker.MessageExchange', lines: 16 - 19

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

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

Exception message output:

// PzdcDoc snippet of: 'ru.bgcrm.worker.MessageExchange', lines: 60 - 64

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

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

Restoring a value:

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

public ActionForward messageUpdate(ActionMapping mapping, DynActionForm form, ConnectionSet conSet)
        throws Exception {
    MessageTypeConfig config = setup.getConfig(MessageTypeConfig.class);

    MessageType type = config.getTypeMap().get(form.getParamInt("typeId"));
    if (type == null)
        throw new BGException("Не определён тип сообщения.");
    
    // сохранение типа сообщения, чтобы в следующий раз выбрать в редакторе его
    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");

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, 
            String.valueOf(searchResultLinked.getPage().getRecordCount()),
            String.valueOf(searchResultLink.getPage().getRecordCount()));
    new IfaceStateDAO(con).compareAndUpdateState(ifaceState, currentState, form);
}

Show in JSP:

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

<c:if test="${processType.properties.configMap.getSok('1', false, 'show.tab.links.process', 'processShowProcessLinks') eq '1'}">
        <c:set var="ifaceId" value="link_process"/>
        <c:set var="ifaceState" value="${ifaceStateMap[ifaceId]}"/>

        <c:url var="url" value="/user/process/link.do">
                <c:param name="action" value="linkProcessList"/>
                <c:param name="id" value="${process.id}"/>
                <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}"/>
        </c:url>

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

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: 158 - 158 --%>

<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="${u:getConfig(ctxSetup, 'ru.bgcrm.plugin.task.Config')}"/> 

Pagination

Putting results into JSP and rendering a form;

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

<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="${form.id}"/>
        
        <ui:page-control nextCommand="; $$.ajax.load(this.form, $('#${uiid}').parent())"/>
</html:form>

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/config.do" method="POST" enctype="multipart/form-data" name="form">
        <input type="hidden" name="action" value="licenseUpload"/>
        <input type="file" name="file" onchange="$(this.form).submit();" style="visibility: hidden; display: none;"/>
        <button type="button" class="btn-grey w100p mt1" onclick="$(this.form).find('input[name=file]').click();">${l.l('Загрузить файл лицензии')}</button>
</form>
<script>
        $(function () {
                $$.ajax.upload('${uploadFormId}', 'lic-upload-iframe', function () {
                        $$.ajax.load('${form.requestUrl}', $$.shell.$content());
                });
        });
</script>

Action:

// PzdcDoc snippet of: 'src/ru/bgcrm/struts/action/admin/ConfigAction.java', lines: 127 - 134

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

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

    return status(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="$$.ajax.post($('#${formUiid}')[0], {toPostNames: ['config', 'matrix']}).done(() => $$.ajax.load('${editUrl}', $('#${formUiid}').parent()));">ОК</button>
        <button type="button" class="btn-grey mr1" onclick="$$.ajax.load('${editUrl}', $('#${formUiid}').parent())">${l.l('Восстановить')}</button>

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

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" />
</c:if>

Action execution result include:

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

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

Flex layout, using constants from Java classes, print button close to a field:

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

<div style="display: flex;">
        <u:sc>
                <%@ 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>
                        </jsp:attribute>
                </ui:combo-single>
        </u:sc>
        <div class="pl05">
                <button type="button" class="btn-white btn-slim" style="white-space: nowrap;"
                        onclick="this.form.elements['searchBy'].value='userId'; $$.ajax.load(this.form, '#searchResult');"
                        title="${l.l('Вывести')}">
                                <%-- &#x25B6; Unicode стрелки вправо, но слишком чёрная --%>
                                &nbsp;<img src="/images/arrow-right.png">
                </button>
        </div>
</div>

Snap-in refresh upon moving back to it:

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

<script>
        $(function () {
                $('#content > #log').data('onShow', function () {
                        $$.ajax.load("/user/log.do", $$.shell.$content());
                });
        });
</script>

Sending AJAX request and blocking a button during a long(er) action execution:

<%-- PzdcDoc snippet of: 'webapps/WEB-INF/jspf/admin/dynamic/dynamic.jsp', lines: 22 - 30 --%>

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

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"/>

Build and publish

Project is configured in Gradle format (configuration file: build.gradle)

For building and publishing, apart from Java you will need console environment with available ant, ssh and rsync packages.

Here and below all commands are shown for WSL environment, *NIX will not need bash -c prefix.

Documentation

bash -c "./gradlew clean buildDoc"

Resulting HTML files will be present in target/doc. Internal link validation is performed automatically.

Change update

Build and publish on https://bgerp.org/update update package with a change.

Public SSH key or the developer might be added for bgerp-cdn@pzdc.de. Before you publish a change update, make sure that ssh bgerp-cdn@pzdc.de sessions work for you.

Clean before publish:

bash -c "./gradlew clean buildClean "

If there are some documentation or Java libraries changes:

bash -c "./gradlew buildUpdateLib buildDoc"

Or only application changes:

bash -c "./gradlew buildUpdate"

For publish operation make the command:

bash -c "./gradlew publishUpdate"

Of course, all the Gradle tasks might be started together, so the typical case is:

bash -c "./gradlew clean buildClean buildUpdate buildDoc publishUpdate"

All the updates packages are copies to Web directory: https://bgerp.org/update/PROCESS_ID The change file has also copied, and all documentation links there starting from https://bgerp.org/doc are automatically replaced to the https://bgerp.org/update/PROCESS_ID/doc.

Users have a capability to install a package using install command, report issues and caveats and, if new version is not operational/suitable, rollback to the latest published version using update command.

Multiple update publications are possible until all bugs/caveats are found - after that that change gets merged (with --squash) into a main branch and a new build gets published

Release

Check Unit tests running.

Public SSH key or the developer might be added for bgerp-cdn@pzdc.de and www@bgerp.org. Before you publish a change update, make sure that ssh bgerp-cdn@pzdc.de and ssh www@bgerp.org sessions work for you.

Build is performed from a master branch and can include several merges (with --squash).

Perform a separate GIT Push for each 'merge --squash' in order to correctly publish in the open repository.

Depending on the presence of 3d-party libraries in an update, perform the following:

bash -c "./gradlew buildClean buildUpdate"

or:

bash -c "./gradlew buildClean buildUpdateLib buildUpdate"

Then:

bash -c "./gradlew patchChanges rss publishBuild publishCommit"

Check the release commit and make:

git push

Merge the latest state of the master on documentation branch.

Docker Image

docker login --username bgerp

Input access token.

Go in directory build/docker

bash files.sh &&
docker build . -t bgerp/bgerp &&
docker push bgerp/bgerp

Tests

Unit

JUnit framework is used (`srcx/test' directory) Used to test specific algorithms, test do not depend on each other and do not work with DB.

Test are launched locally using the following command:

bash -c "./gradlew clean test"

Integration

TestNG framework is used (`srcx/itest' directory)

Integration test performs initialization an empty DB and filling after it with configuration. Tests form a dependencies graph which defined order and execution parallelism. Upon successful tests execution, written in DB dump gets extracted for https://demo.bgerp.org

./gradlew integrationTest -Pdb.host=DB_HOST -Pdb.user=DB_USER -Pdb.pswd=DB_PSWD

Parameters DB_HOST, DB_USER, DB_PSWD are used for accessing the MySQL server, where a test DB will be created. Use Docker DB Instance for the that.

Optimization

Addtionally to the recommended MySQL configuration, check and set in my.ini:

innodb_file_per_table=0

That will significantly increase table creation speed. After the first successful run, the structure of a DB will remain persistent and re-created faster by this way.

Creation of dump for Windows:

echo DROP DATABASE IF EXISTS bgerp; > ./dump.sql &&
echo CREATE DATABASE bgerp DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; >> ./dump.sql &&
echo USE bgerp; >> ./dump.sql &&
mysqldump -uDB_USER -pDB_PSWD bgerp --add-drop-database --no-data >> ./dump.sql &&
type build\bgerp\db_init_end.sql >> ./dump.sql

For *NIX:

echo "DROP DATABASE IF EXISTS bgerp;" > ./dump.sql &&
echo "CREATE DATABASE bgerp DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;" >> ./dump.sql &&
echo "USE bgerp;" >> ./dump.sql &&
mysqldump -uDB_USER -pDB_PSWD bgerp --add-drop-database --no-data >> ./dump.sql &&
cat build/bgerp/db_init_end.sql >> ./dump.sql

Running the tests after:

mysql -uDB_USER -pDB_PSWD < ./dump.sql &&
gradlew integrationTest -Pdb.user=DB_USER -Pdb.pswd=DB_PSWD -Pskip.dbReset=true

GitLab CI

.gitlab-ci.yml file has configuration for runnign certain tasks automatically upon each GIT commit. Different tasks are executed within different Workflow branches. Please find below description of CI tasks

test-unit-and-doc

Launches Unit tests and <<build-doc, documentation build and validity check>.

test-integration

publish-doc

Launches documentation build and validity check, if no errors are found, publishes on https://bgerp.org/doc/3.0/manual.

publish-source

Publishes actual source code from `master`into an open repository https://github.com/pingvin235/bgerp This method has been chosen in order to limit an open repository size and in order to hide previously deleted files from history.