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

Development environment

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

  • Windows - currently used by the maintainer;

  • Linux;

  • MacOS.

Minimal required set of software:

Bash 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 - embedded in Windows virtual Linux machine, tested/approved;

  • WSL 2 - the new version of it, not tested yet;

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

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

Format

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

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 bit faster than Eclipse, has better JavaScript and Gradle support.

Drawbacks:

  • JSP support is nearly absent;

  • external GIT client is required.

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

Running in IDE

Configuration:

  • Main class: ru.bgerp.Server

  • Program arguments: start

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

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

Copy bgerp.properties to bgerp_test.propertes, replace GENERATED_PASSWORD with some random value, possibly using pwgen or similar utility.

Database

MySQL server may be running on any supported platform, check configuration of it.

For database creation use the following files sequentially:

Replace GENERATED_PASSWORD in db_create.sql with the value from bgerp_test.properties but surrounded by commas.

Execute the following mysql commands:

mysql --default-character-set=utf8 -uDB_USER -p < db_create.sql
mysql --default-character-set=utf8 -ubgerp -p < db_init.sql
mysql --default-character-set=utf8 -ubgerp -p bgerp < bgerp.sql

For DB update on each start in .properties must be set option:

runOnStart+=,ru.bgcrm.util.distr.DevDbUpdater

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 DB_USER (will not be handled by GIT) and make required changes.

Files

Project’s folder listing along with decription.

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

Основная идея - мерджи производятся только "вниз": с основной ветки на вторичные для получения актуального состояния. На основную ветку всё переносится посредством патчей, поэтому она имеет линейную структуру. Вторичные ветки впоследствии могут быть удалены, поскольку вся агрегированная информация из них содержится в основной.

Ветка Базируется на Назначение CI

master

Стабильная версия программы.

p<ID процесса>-short-description

master

Изменение программы и документации.

p11862-documentation

master

Корректировки документации. Долгоживущая ветка, периодически переносится на master.

documentation-change-request

p11862-documentation

Предложения по корректировке документации, могут быть смерджены обратно на p11862-documentation.

Основная ветка проекта - master, с неё собираются сборки.

  • Каждое изменение должно базироваться на процессе в BGERP BiTel, в котором происходит весь обмен информацией по нему.

  • Идентификатор изменения - строка p<ID процесса>, например p13455

  • Для каждого изменения создаётся отдельная ветка GIT на базе основной, название начинающееся с идентификатора изменения, разделители - дефисы. Например: p11788-link-filter-title

  • В процессе разработки в ветке допускается любая стратегия коммитов: промежуточные коммиты, ответвления, откаты коммитов. Рекомендуется пушить промежуточные состояния, используя GIT репозитарий как резервную копию.

  • На ветку изменения могут быть периодически смерджена основная ветка для синхронизации с актуальным состоянием и обязательно перед переносом.

  • Необходимые правки документации производятся одновременно с модификацией исходного кода.

  • Пакет тестового обновления для клиента, равно как и документацию, можно собирать и опубликовать из ветки.

  • Ветка должна добавлять файл build/change.<ID процесса>.txt, при его отсутствии он создаётся автоматически при сборке изменения. В файле на разных строках должны быть описаны новые функции, исправления и прочие изменения. Формат идентичен с changes.txt, в который информация переносится автоматически при публикации обновления.

  • По завершению разработки и тестирования у клиента необходимо запросить перенос ветки с изменением на основную ветку, для этого процесс BGERP перевести в статус Приёмка.

  • После проверки процесс закрывается, а изменение переносится в виде единственного коммита, с комментарием начинающимся с идентификатора изменения в основную ветку.

  • Ветка разработки впоследствии может быть удалена. Автор изменения может быть установлен по идентификатору в комментарии.

Невозможно одновременно тестировать несколько изменений параллельно на одной системе и пользоваться ими. Каждое изменение будет перетирать другое при установке обновления. В крайнем случае допустимо создание сборочных веток, на которые смердживаются несколько веток изменений.

Настройка GIT

Пример настройки в файле $USER_HOME\.gitconfig:

[user]
        email = shamil@company.com
        name = Shamil Vakhitov
[credential]
        helper = store
[core]
        autocrlf = false
        fileMode = false
[pull]
        rebase = true

NOTE:

Команды GIT

Почистить все ссылки на несуществующие более удалённые ветки:

git remote prune origin

Получение последних обновлений основной ветки не будучи в ней:

git fetch origin master:master

Приёмка и перенос

При принятии изменения производится сравнение ветки с основной. Поэтому перед передачей изменения необходимо смерджить на ветку актуальное состояние основной.

branch compare

Также необходимо создать и заполнить файл с описанием изменений. Для этого может быть использована команда:

bash -c "./gradlew touchChanges"

GIT команды для переноса изменений из ветки pXXXXX-short-change-description в основную:

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

git push

Периодически с основной ветки публикуется обновление.

Долгоживущие ветки

В долгоживущих ветках производятся изменения не влияющие на функциональность продукта. Как-то: корректировка локализации, документации, форматирование кода, тесты. Процесс изменения при этом не закрывается, а перенос производится многократно по мере готовности очередной порции изменений.

Documentation

A particular case of long-life 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 comparism, find the new rows and change them in the .adoc:

snippet fix 2

Architecture

BGERP представляет из себя standalone Java приложение с динамическим Web интерфейсом. Запуск во время разработки может быть произведён прямо в IDE.

Принципиальная структура обработки запросов изображена ниже.

diag d0b545adbd700daf7b25e599b723ed1d

Этапы обработки:

  1. Запрос формируется с помощью JS из HTML формы и отправляется в метод класса Java Action.

  2. На запрос изменения отправляется только подтверждение - JSON документ со статусом OK.

  3. Любое исключение в процессе работы Action приводит к отправке на клиентскую сторону JSON со статусом ERROR.

  4. На запрос чтения данных в случае корректной обработки отправляется фрагмент HTML документа, встраиваемый на клиенте в нужное место. Например, таблица со списком пользователей.

Стек используемых технологий в порядке убывания важности:

  1. Java - вся логика реализуется в Java коде, поскольку он предоставляет лучшие параметры по удобству разработки, надёжности и быстродействию;

    1. MySQL + JDBC - работа с данными;

  2. HTML + CSS - язык разметки в браузере и таблицы стилей;

  3. JSP + JSTL - шаблоны отрисовки HTML страниц, уровень представления;

  4. JS + JQuery - скриптовый язык для динамических возможностей на странице браузера, используется минимально в виде готовых вызовов фреймворка.

Избегать смешения слоёв: логики вне Java, представления вне JSP, манипулирования данными вне MySQL.

Database

Структура базы документируется в общем процессе, одновременно c внесением изменений. В приложение база создаётся и обновляется SQL скрипто build/update/db.sql. При запуске в IDE этот скрипт может быть выполнен автоматически при каждом старте.

Дополнительно плагины могут содержать собственные скрипты, выполняемые в момент их инициализации.

Cache

Кэширование во внутренних Map и List используется для ускорения в сотни раз получения справочных значений при построении UI, либо JEXL скриптах. Map объекты позволяют получать объекты по коду, List - упорядоченный по алфавиту список значений. Примеры кэшируемых объектов: пользователи, их группы, типы процессов, конфигурации.

Рекомендуется всегда когда возможно использовать в первую очередь кэши для получения справочников, избегая лишних запросов к БД.

Редактирование справочников происходит напрямую с БД, после редактирования кэш сбрасывается.

// 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);
    }

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’s 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 классы определяются в struts-config.xml файлах, и привязываются к URL запроса. Имя метода передаётся в HTTP параметре запроса action. Если параметр отсутствует, то вызывается unspecified метод, подобный способ нежелателен.

Пример объявления акшена с форвардом:

<!-- PzdcDoc snippet of: 'src/ru/bgerp/plugin/blow/struts-config.xml', lines: 4 - 8 -->

<action path="/user/plugin/blow/board" parameter="action" type="ru.bgerp.plugin.blow.struts.action.BoardAction" name="form" scope="request">
        <forward name="board" path="/WEB-INF/jspf/user/plugin/blow/board/board.jsp"/>
        <forward name="show" path="/WEB-INF/jspf/user/plugin/blow/board/show.jsp"/>
        <forward name="search" path="/WEB-INF/jspf/user/plugin/blow/board/search.jsp"/>
</action>

Акшены должны расширять класс ru.bgcrm.struts.action.BaseAction, методы возвращать результат через вызов data либо status. Первый метод обрабатывается JSP страницей-форвардом и высылает HTML на клиент. Второй - предназначен для выполнения изменений и возвращает только JSON формат с результатом выполнения.

Form Object

В каждый вызов акшена передаётся супер объект form ru.bgcrm.struts.form.DynActionForm, содержащий контекст выполнения:

  • пользователь;

  • параметры запроса, вспомогательные методы для их разбора.

Не использовать устаревший формат акшенов с параметрами HttpClientRequest и HttpClientResponse, они есть в form.

Фрагмент читающего акшен метода с форвардом.

// PzdcDoc snippet of: 'ru.bgerp.plugin.blow.struts.action.BoardAction', lines: 38 - 58

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, mapping, form, "show");
}

Результат перенаправляется на JSP страницу: webapps/WEB-INF/jspf/user/plugin/blow/board/show.jsp.

В этом же form следует передавать данные для отрисовки JSP, за исключением различных вспомогательных справочников. Для этого используется поле response формы. При responseType=json, всё отправленное в response сериализуется в JSON, именно поэтому справочники следует помещать в 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");
}
Именование

Классы акшенов плагинов должны располагаться в пакете PLUGIN_PACKAGE.action, имя класса оканчиваться на Action. Ранее акшены располагались в пакетах struts.action, такой подход является устаревшим.

Акшены интерфейса user разделяются на /user и /admin. Деление это в будущем возможно будет использовано для выделения административных вызовов.

Интерфейсы usermob и open содержат свои акшены, в имени пакетов и URL которых должно располагаться usermob и open соответственно. Пример такого акшена org.bgerp.action.usermob.ProcessAction

Рекомендуемая схема именования методов:

  1. опционально имя обрабатываемого объекта, если класс работает с несколькими объектами;

  2. глагол, определяющий операцию, для CRUD это: list, get, update, delete;

Примеры имён методов из ru.bgcrm.struts.action.admin.UserAction:

  • permsetList;

  • permsetGet;

  • permsetUpdate.

  • status - статус приложения;

  • update - установка обновления;

  • userLoggedList - список авторизовавшихся пользователей.

Форвард рекомендуется называть так же как метод:

<!-- PzdcDoc snippet of: 'webapps/WEB-INF/struts-config.xml', lines: 55 - 58 -->

<action path="/admin/app" parameter="action" type="ru.bgcrm.struts.action.admin.AppAction" name="form" scope="request">
        <forward name="status" path="/WEB-INF/jspf/admin/app/status.jsp"/>
        <forward name="userLoggedList" path="/WEB-INF/jspf/admin/app/user_logged_list.jsp"/>
</action>
Проверка прав

Все методы акшенов должны быть объявлены в файле ru/bgcrm/model/user/action.xml для ядра либо для плагина. Пример объявления акшенов плагина Blow: ru/bgerp/plugin/blow/action.xml Действия из данных файлов образуют дерево, использующееся для разграничения доступа.

Каждое действия идентифицируется классом и методом, разделёнными точкой с запятой. Через точку с запятой перед основным идентификатором можно указать дополнительные, например, при переименовании классов либо методов. Это позволяет обеспечить обратную совместимость ранее записанных в БД привелегий. При следующем сохранении набора прав будет использован новый идентификатор.

<!-- PzdcDoc snippet of: 'ru/bgcrm/model/user/action.xml', lines: 145 - 146 -->

<item title="Приложение">
        <item action="ru.bgcrm.struts.action.admin.AppAction:status, ru.bgcrm.struts.action.admin.StateAction:null" title="Статус"/>

Вспомогательные действия, которые должны быть постоянно разрешены помечаются атрибутом allowAll="1".

Обработка ошибок

Исключение прерывает выполнение акшена, прерывает тразакцию в БД, ответ отправляется всегда в JSON формате. Обработка исключений производится централизованно в ru.bgcrm.struts.action.BaseAction, методы DAO либо обработчики скриптов должны просто выбрасывать их все наружу. Соответственно в декларациях методов акшенов и DAO должно значиться throws java.lang.Exception.

Класс ru.bgcrm.model.BGException используется для исключений, генерируемых самой системой, в чистом виде нужен весьма редко.

Его наследник класс ru.bgcrm.model.BGMessageException используется для отправки локализованного уведомления пользователю, без записи сообщения в лог. Пример:

// PzdcDoc snippet of: 'ru.bgcrm.event.listener.ProcessClosingListener', lines: 39 - 41

for (Process link : result)
    if (link.getCloseTime() == null)
        throw new BGMessageException("Есть незакрытые привязанные процессы типа: %s", linkType);
Работа с БД

Работа с базой данных выполняется через отдельные Java DAO (Data Access Objects) классы. Непосредственно в акшенах используются уже они. Обработка запросов акшенами производится транзакционно: транзакция начинается перед вызовом метода, далее подверждается (COMMIT) при возврате результатов без ошибок либо отменяется (ROLLBACK) при выбросе исключения.

Соединение с базой передаётся в параметрах con акшен методов. Также есть варианты методов с параметром conSet (ru.bgcrm.util.sql.ConnectionSet), выдающим соединения по отдельному вызову. Данный способ подходит для методов не требующих соединения с БД, либо же наоборот, требующих несколько видов соединений: реплики или мусорной для некритичных данных БД (пока не поддержана).

Имена таблиц указываются в константах, для повышения связанности кода, например: ru.bgcrm.dao.Tables Если таблица используется только в единственном DAO классе, то константу с её именем делать приватной: ru.bgcrm.plugin.fulltext.dao.SearchDAO

Для сборки запросов рекомендуется использование класса java.sql.PreparedStatement либо более удобной его обёрткой ru.bgcrm.util.sql.PreparedDelay[]. Второй класс удобен возможностью склеивать как запросы, так и параметры и не указывать номер позиции устанавливаемых параметров.

JSP View

HTML страницы отрисовываются на серверной стороне JSP шаблонами, получая даннные от Action. UI реализован из унифицированных компонентов таким образом, что в большинстве случаев что не требуется устанавливать стили, писать дополнительные JS обработчики. Примеры кода JSP UI.

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 autocomplition for them.

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

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

Идентификация элементов

В HTML DOM все идентификаторы элементов глобальные с использованием атрибутов class и id. Их довольно сложно отслеживать на предмет используемости и уникальности, а кроме того приложение обязательно должно быть одностраничным, что мешает сохранять состояние на скрытых элементах. Для обхода этого неудобства в проекте используется привязка JS обработчиков через HTML атрибуты onClick и т.п., с применением конекстной переменной this. Либо генерируется уникальный идентификатор с помощью JSP функции u:uiid(), который также подставляется в сгенерированный вызов JS.

Tags

Компоненты выполнены как JSP теги, объявлены в каталоге webapps/WEB-INF/tags. IDE Eclipse поддерживает автодополнение при их использовании.

ide jsp tag

Обзор использования тегов с элементами управления пользователя вы можете посмотреть в файле webapps/test.jsp, для выполнения шаблона наберите в браузере http://<host>:<port>/test.jsp, для Demo системы: https://demo.bgerp.org/test.jsp

Пункты меню, устанавливающие соответствие между URL оснастки и Java Action определяются для ядра в файле webapps/WEB-INF/jspf/user/menu.jsp, для плагинов - через точку расширения.

u:sc

JSP страница хранит все переменные глобально. В сочетании с инклудами и большими шаблонами это может создавать неудобства. Для обхода проблемы используется тег восстановления контекста <u:sc>, все переменные созданные внутри него сбрасываются при выходе.

p:check

The tag enabling code inside 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: 34 - 35 --%>

<shell:title ltext="Конфигурация"/>
<shell:state help="kernel/setup.html#config"/>

Java Script

JS используется для обеспечения динамического поведения на странице клиента. Скрипты и библиотеки к ним располагаются по следующим путям, плагины могут обладать собственными скриптами.

В некоторых местах код JS генерируется на стороне сервера в JSP шаблонах. Данный способ не очень предпочтителен, поскольку сложен для отладки в браузере.

Функции системы разбиты по объектам, выстроенных в иерархию от корневого объекта $$. Например: $$.ajax, $$.ui Обычная схема привязки JS кода к HTML: установка обработчиков событий с вызовом функций, пример AJAX.

Устаревшие JS функции помечаются следующим образом:

// PzdcDoc snippet of: 'webapps/js/kernel.ajax.js', lines: 352 - 357

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

        openUrlPos( url, selectorStart, "last" );
}

При их вызове в console бразузера выводится сообщение: "Deprecated", при клике по которому можно найти устаревший вызов. Использование подобных функций не допускается в новом коде и должно уменьшаться в существующем.

Плагины

Всякая логически обособленная функциональность должна быть вынесена в плагин. Особенностью плагинов является их изолированность. Каждый плагин работает с ядром, ядро не знает особенностей конкретного плагина, плагины не знают друг о друге. Под знанием тут подразумевается полагание на конкретное API, вызовы.

Java класс плагина

Плагины находятся программой при старте по обязательному Java классу, расширяющему ru.bgcrm.plugin.Plugin Пример класса: ru.bgerp.plugin.blow.Plugin

Каждый плагин должен уникальным Java package, содержащим данный класс. Далее пакет плагина будет обозначаться как PLUGIN_PACKAGE.

В методе инициализации класса плагина могут определяться слушатели событий:

// PzdcDoc snippet of: 'ru.bgcrm.plugin.slack.Plugin', lines: 21 - 23

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

В данном случае в JEXL контекст помещается обработчик функций.

Каждый плагин должен обладать уникальным строковым идентификатором, удовлетворяющим следующим условиям:

  • одно уникальное для данного плагина английское слово без перемены регистра и спецсимволов;

  • таблицы БД плагина, если они есть, должны начинаться с идентификатора;

  • actions плагина, если они есть, должны начинаться с /user/plugin/идентификатор.

Если плагин использует БД, то в его пакете может находиться скрипт, создающий и изменяющий таблицы. Скрипт с именем db.sql вызывается при стандартной процедуре инициализации.

Пример скрипта для плагина FullText.

// 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 !;

XML декларация

Необязательный файл plugin.xml, располагающийся в PLUGIN_PACKAGE

В файле определяются точки расширения:

  • JSP шаблоны;

  • JS файлы.

Пункты меню

Объявление точки расширения в XML декларации:

<!-- PzdcDoc snippet of: 'blow/plugin.xml', lines: 4 - 4 -->

<endpoint id="user.menu.items.jsp" file="/WEB-INF/jspf/user/plugin/blow/menu_items.jsp"/>

Добавление пункта с помощью тега JSP: webapps/WEB-INF/jspf/user/plugin/blow/menu_items.jsp

JavaScript

Объявление точки расширения в XML декларации:

<!-- PzdcDoc snippet of: 'blow/plugin.xml', lines: 5 - 5 -->

<endpoint id="js" file="/js/pl.blow.js"/>

Файл со скриптом: webapps/js/pl.blow.js

Actions

Акшены плагина, если требуется, должны быть определены в PLUGIN_PACKAGE.action каталоге. Файлы struts-config.xml и action.xml должны находится в PLUGIN_PACKAGE.

JSP шаблоны - webapps/WEB-INF/jspf/user|open|usermob/plugin/идентификатор.

Локализация

Файл локализации плагина i10n.xml должен находиться в PLUGIN_PACKAGE. Он используется только для локализаций акшенов и JSP шаблонов в них.

Кастомизация

Одним из изначальных приоритетов системы была расширяемость и гибкость. Поэтому стандартный функционал может быть расширен несколькими способами.

Следует однако понимать, что наиболее эффективный путь разработки и долгосрочной поддержки функциональности - реализация в виде штатного плагина либо части ядра, с вынесением необходимого минимума параметров в конфигурацию. JEXL скрипты либо динамический код могут быть использованы для быстрого прототипирования, либо реализации исключительно специфичной для данной инсталляции логики.

Практика показывает, что из всего набора кастомизированных попыток постепенно выявляются удачные решения, которые подходят значительному числу пользователей. Такие необходимо переносить в основной код, делая частью системы и совместно развивая её дальше.

Следующая диаграмма визуально отображает данную динамику трансформации совокупной массы программного кода разных типов. Объём кода учитывается для всех клиентов, бОльший объём из разрозненных решений преобразуется в меньший объём более универсального кода единого продукта.

diag fa60097de6559303acd31dfacc9aab84

Localization

Все сообщения в логах не локализуются и выводятся на английском языке.

Локализуется интерфейс и сообщения, адресованные пользователю системы. Язык системы задаётся глобально в конфигурации.

Files

XML файл локализации l10n.xml для ядра располагается в src/ru/bgerp/l10n, для плагинов - в их пакетах . Новые локализирующие фразы должны добавляться в конец списка в файле.

Для отключения в момент разработки кэширования локализаций установите в конфигурации localization.cache=0

Ключом локализирующей фразы выступает первая запись на любом языке, например русском:

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

Возможно использование коротких сокращённых ключей, представляя их в виде записей на особом системном языке, например:

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

Для изменения надписей на кнопках в интерфейсе, следует применять добавление отдельной локализации (изменять русскую локализацию не получится, так как она испольузется как ключ), например меняем кнопку в мастере создания, через язык my:

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

Code

Локализация может выполняться в JSP шаблонах и Java акшенах. В JS коде локализация доступна, только если он генерируется JSP.

В коде JSP шаблона вызов локализации из примера выше выглядит следующим образом:

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

При выполнении акшена в объект l передаётся локализационный контекст, содержащий фразы для ядра и вызываемого плагина.

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

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 figuring out missing keys add in logging configuration:

log4j.appender.l10n=org.apache.log4j.ConsoleAppender
log4j.appender.l10n.Target=System.out
log4j.appender.l10n.layout=org.apache.log4j.PatternLayout
log4j.appender.l10n.layout.ConversionPattern=%d{MM-dd/HH:mm:ss} %5p [%t] %c{1} - %m%n

log4j.logger.ru.bgerp.l10n.Localizer=DEBUG, l10n
log4j.additivity.ru.bgerp.l10n.Localizer=false

Именование

Переменные конфигураций, функции JS, HTTP StyleId

  • Все переменные конфигурации от плагинов начинать как <plugin>:

  • Все функции JS плагинов начинать с префикса <plugin>- В других местах тире запретить в названии функции.

  • Все идентификаторы (style id) DOM элементов для плагинов начинать как <plugin>- В других местах тире запретить в названиях идентификаторов.

База данных

1) Таблицы и поля в них именовать с нижним подчёркиванием. process_id param_id

Переменные Java, параметры HTTP запросов, переменные в JSP

В camelCase нотации: processId paramId

Дата - поле сущности

Если в поле только дата, то: createDate - Java - тип java.util.Date create_date - БД - тип date

Если в поле дата + время, то: createTime - Java - тип java.util.Date create_dt - БД - тип datetime

Дата - период сущности

В бинах дату периода хранить с типом java.util.Date с именем: dateFrom dateTo

Соответственно методы получения и установки: setDateFrom setDateTo getDateFrom getDateTo

Не использовать для хранения в бинах Calendar. Если нужно конверить в календарь или из календаря - использовать TimeUtils.

Если нужно хранить время, то делаем: timeFrom timeTo

Также тип java.util.Date.

Calendar использовать во всяких калькуляторах/тарификаторах, когда реально нужно постоянно двигать дату.

В базе использовать поля from_date и to_date, тип date.

from_dt, to_dt - тип datetime.

Неограниченные даты - NULL.

Code samples

Так как система очень быстро меняется, то здесь собираются примеры актуального кода, рекомендуемого к использованию. Сниппеты извлекаются непосредственно из исходных файлов проекта, поэтому всегда достоверны. При необходимости воспользуйтесь IDE для поиска классов или файлов.

Сохранение последних параметров запроса пользователя

Например, фильтров интерфейса. Используется метод restoreRequestParams в: ru.bgcrm.struts.action.BaseAction

Сохранение значения:

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

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

Восстановление:

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

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");
    }

Сохранение плюс восстановление сразу:

// PzdcDoc snippet of: 'ru.bgcrm.struts.action.ProcessLinkAction', lines: 52 - 56

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

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

Отображение на вкладке количества элементов

Например, количества связанных процессов. Сохраняется при первом вызове. Используется класс ru.bgcrm.model.IfaceState

Обновление значения:

// PzdcDoc snippet of: 'ru.bgcrm.struts.action.ProcessLinkAction', lines: 220 - 227

// проверка и обновление статуса вкладки, если нужно
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

При форматировании дат и времени в Java либо JSP для независимости от текущей локали и унификации используются форматы вида:

  • ymd - год, месяц, день;

  • ymdh - год, месяц, день, час;

  • ymdhm - год, месяц, день, час, минута;

  • ymdhms - год, месяц, день, час, минута, секунда.

В Java коде для форматирования дат используется класс ru.bgcrm.util.TimeUtils, в нём же есть константы с форматами.

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

Бины конфигурации

Для ускорения парсинга и валидации используются Java объекты с классом, наследующим ru.bgcrm.util.Config. Например: ru.bgcrm.model.config.IsolationConfig. Данная конфигурация поддерживает конструктор с флагом валидации, что позволяет проверять синтаксис при сохранении.

Бины конфигурации могут быть получены и в 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')}"/> 

Logging

Java

Используется класс логгер ru.bgerp.util.Log, базирующийся на фрейморке Log4j. При запуске в IDE конфигурационный файл из дистрибутива build/bgerp/files/log4j.properties может быть скопирован в корень проекта и изменён требуемым образом.

Для логирования в actions использовать protected переменную log.

В Java классах создавать static final переменную класса:

// 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();

Вывод сообщения при исключении:

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

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

JS

Вывод отладки вместо 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,

Постраничный вывод

Вывод результатов в JSP и отображение формы:

<%-- 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: 211 - 213

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

JSP UI

Простой справочник с промотчиком страниц, вызов редактора AJAX: webapps/WEB-INF/jspf/admin/process/status/list.jsp

Отправка AJAX для сохранения, выхода, либо восстановления данных в редакторе свойств типа процесса:

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

<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())">Восстановить</button>
                
        <button type="button" class="btn-grey ml1" onclick="$$.ajax.load('${form.returnUrl}', $('#${formUiid}').parent())">К списку типов</button>
</div>

Рекурсивный инклуд:

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

Инклуд результата выполнения акшена:

<%-- 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, использование констант из Java классов, кнопка вывода рядом с полем:

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

Обновление оснастки при повторном переходе в неё:

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

Отправка AJAX запроса, блокировка кнопки при долгом выполнении действия:

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

Проект сконфигурирован в формате Gradle, конфигурационный файл build.gradle. For build and publish except Java is required console enviroment with available ant, ssh and rsync packages.

Здесь и далее команды приведены в расчёте на WSL окружение, в *NIX системах префикс bash -c не требуется.

Документация

bash -c "./gradlew clean buildDoc"

Собранные HTML файлы доступны в каталоге target/doc. Они автоматически проверяются на корректность внутренних ссылок.

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.

Пользователи имеют возможность установить пакет с помощью команды install, сообщить о выявленных недочётах и откатиться на предыдущую версию командой update в случае невозможности работать с ними далее. Возможна многократная публикация до выявления всех недочётов, после чего именение переносится в основную ветку и публикуется очередное обновление.

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.

Сборка производится с master ветки и может включать несколько изменений, перенесённых на неё.

Для каждого переноса делать отдельный GIT push, для корректной публикации в открытом репозитарии.

В зависимости от наличия в обновлении сторонних библиотек выполнить команды:

bash -c "./gradlew buildClean buildUpdate"

либо:

bash -c "./gradlew buildClean buildUpdateLib buildUpdate"

Далее:

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, каталог srcx/test. Проверка отдельных алгоритмов, тесты не зависят друг от друга, не работают с БД.

Запуск локально командой:

bash -c "./gradlew clean test"

Integration

Используется фреймворк TestNG, каталог srcx/itest.

Интеграционный тест производит сборку, установку и запуск приложения с реальной БД. Далее в браузере эмулируется различные действия пользователя с проверкой результата. Тесты образуют граф зависимостей, определяющий порядок и параллельность выполнения. После успешного прохождения тестов дамп заполненной БД выгружается для https://demo.bgerp.org

The tests recreate DB with name bgerp
./gradlew integrationTest -Pdb.host=DB_HOST -Pdb.user=DB_USER -Pdb.pswd=DB_PSWD

Parameters DB_HOST, DB_USER, DB_PSWD - for accessing the MySQL server, there the test DB will be created.

Optimization

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

innodb_file_per_table=0

That will significally increase table creation speed. After the first successfully run, the structure o DB may be persisted 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 настроен автоматический запуск задач на каждый коммит в GIT. Различные задачи выполняются в разных ветках Workflow. Ниже описание CI задач.

test-integration

publish-doc

Запускает сборку и проверку документации, при отсутствии ошибок - публикует её на https://bgerp.org/doc/3.0/manual.

publish-source

Публикует актуальные исходные коды из master в открытый репозитарий https://github.com/pingvin235/bgerp Данный способ выбран для уменьшения размера открытого репозитария и сокрытия удалённых ранее файлов из истории.