Проект сконфигурирован в формате Gradle, конфигурационный файл build.gradle.

Настройки IDE

Форматирование

IDE либо редактор должны быть настроены для отображения символов форматирования.

ide format

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

Не рекомендуется совмещать форматирование файлов с внесением изменений, это существенно усложняет анализ в дальнейшем.

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

Java

Форматирование Java кода должно соответствовать Java Conventions со следующими изменениями. Описано для форматера Eclipse.

Настройки в Windows - Preferences - Java - Code style - Formatter. Необходимо открыть стандартный форматтер и сохранить под новым именем, изменив параметры:

  • Indentation - Tab policy - Spaces only

  • Indentation - Tab size - 4

  • Line Wrapping - Maximum line width - 150

Window - Preferences - Java - Code style - Organize Imports в двух полях поставить 99 и 1.

Готовый файл форматера в формате Eclipse: formatter.xml

Eclipse

В данный момент наиболее удобное решение. Загрузить Eclipse for Java EE, эта сборка уже содержит GIT клиент, редакторы JSP и XML.

Установить плагины:

  • Buildship Gradle - поддержка Gradle;

  • AsciiDoctor - правка документации;

  • MoreUnit - удобная работа с Unit тестами.

Вызвать правым кликом на проекте меню Refresh Gradle Project. Его же вызывать при любом изменении библиотек проекта.

Window - Preferences - General - Editors - Text Editors установить галочку Show whitespace characters

Импортировать форматер Java в Window - Preferences - Java - Codestyle - Formatter.

VS Code

Более быстрая чем Eclipse, лучше поддержка JavaScript, Gradle. Недостатки:

  • почти нет поддержки JSP;

  • хуже редактор AsciiDoc.

Примеры настройки можно посмотреть в каталоге .vscode в корне проекта. Форматер Java и отображение пробельных символов там уже включены.

Запуск

Параметры для запуска:

  • Main class: ru.bgerp.Server

  • Program arguments: start

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

  • Classpath: User entries - Advanced - Add folders нажать и добавить каталог текущего проекта.

bgerp_test.propertes файл должен лежать в корне проекта. Его можно скопировать из bgerp.properties, он не будет сохранён в GIT.

Изначальную БД взять из артифактов последнего запуска integration-test на основной ветке: http://git.pzdc.de/bgerp/bgerp/-/jobs

GIT workflow

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

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

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

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

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

  • В ветке может быть изменён файл .gitignore для хранения кастомизаций в GIT, они не будут перенесены в основную ветку.

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

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

  • Пакет тестового обновления для клиента, равно как и документацию, собирать в ветке и передавать через WEB каталог, содержащий идентификатор процесса. Например: http://bgerp.ru/patch/p10646

  • В верхнюю строку файла build/changes.txt дописывается краткое сообщение об изменении. Префикс обозначает характер изменения.

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

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

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

Невозможно одновременно тестировать несколько изменений параллельно на одной системе BGERP и пользоваться ими. Каждое изменение будет перетирать другое при установке обновления.

Настройка GIT

Пример настройки .gitconfig:

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

GIT команды

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

git remote prune origin

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

git fetch origin master:master

Сборка

Необходим установленный Apache Ant и Perl с модулями. На Linux, WSL, либо Mac.

Сборка пакета обновления, например в разрабатываемой ветке. В каталоге build/update выполнить:

bash -c ant

Сборка пакета обновления библиотек, можно также выполнять в отдельной ветке. Необходимо вызвать перед публикацией, если изменился состав библиотек. В каталоге build/update_lib выполнить:

bash -c ant

Сборка пакета обновления и публикация. Вызвать каталоге build для WSL:

bash -c build_and_copy.sh

Сборка и копирование JavaDoc, в том же каталоге:

bash -c create_doc.sh

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

Исходные файлы в формате AsciiDoctor размещаются в каталоге srcx/doc проекта. Модификация исходных кодов программы и документации выполняется одновременно в ветке изменения. Примеры форматирования и рекомендации можно посмотреть здесь.

Сборка выполняется командой:

gradlew clean buildDoc

Собранные HTML файлы доступны в каталоге target/doc.

Правка документации без привязки к изменениям (см. GIT workflow) происходит в ветке documentation, откуда при каждом коммите происходит публикация на: http://www.bgerp.ru/doc/3.0/manual Ветка master периодически мерджится на ветку документации, откуда в неё попадают все правки связанные с изменениями.

Код

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

diag d0b545adbd700daf7b25e599b723ed1d

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

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

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

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

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

Actions

Определяются в struts-config*.xml файлах, имя метода передаётся в параметре action.

Пример объявления action с форвардом.

<!-- PzdcDoc snippet of: 'webapps/WEB-INF/struts-config-blow.xml', lines: 4 - 7 -->

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

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

Все параметры запроса передаются в объекте ru.bgcrm.struts.form.DynActionForm, он содержит вспомогательные методы для получения параметров разных типов.

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

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

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

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);
        
        updatePersonalization(form, con, persMap -> persMap.put("blowBoardLastSelected", String.valueOf(form.getId())));
    }
 
    return processUserTypedForward(con, mapping, form, "show");
}

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

В этом же form следует передавать данные для отрисовки JSP, за исключением различных вспомогательных справочников. Для этого используется поле response формы. При responseType=json, всё отправленное в response сериализуется в JSON, именно поэтому справочники следует помещать в HttpResponse. Если метод не требует соединения к БД - используйте метод с параметром ru.bgcrm.util.sql.ConnectionSet.

// 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 processUserTypedForward(conSet, mapping, form, "messageList");
}

Исключение прерывает выполнение акшена, возвращается ошибка. Всегда в JSON формате. Исключение с классом ru.bgcrm.model.BGMessageException используется для прерывания акшена с высылкой уведомления. То есть это штатная ситуация. В методах акшенов рекомендуется указывать throws java.lang.Exception, для генерации своих исключений - new BGExeption().

Пример акшена с обработкой параметров, исключениями и локализацией.

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

public ActionForward move(ActionMapping mapping, DynActionForm form, Connection con) throws Exception {
    int processId = form.getParamInt("processId");
    int parentProcessId = form.getParamInt("parentProcessId");
    int fromParentProcessId = form.getParamInt("fromParentProcessId");
    
    ProcessLinkDAO linkDao = new ProcessLinkDAO(con, form.getUser());
    // remove link
    if (fromParentProcessId > 0)
        linkDao.deleteLink(new CommonObjectLink(fromParentProcessId, Process.LINK_TYPE_MADE, processId, ""));
    
    // add link
    if (parentProcessId > 0) {
        linkDao.addLink(new CommonObjectLink(parentProcessId, Process.LINK_TYPE_MADE, processId, ""));
    
        if (linkDao.checkCycles(parentProcessId))
            throw new BGMessageException(l.l("Циклическая зависимость"));
    }
    
    return processJsonForward(con, form);
}

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

Именование

Переменные конфигураций, функции 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.

Примеры

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

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

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

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

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

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

        Message message = null;
        MessageType type = null;
        
        restoreRequestParams(conSet.getConnection(), form, true, false, "messageTypeAdd");

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

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

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.ProcessAction', lines: 1086 - 1090

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

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

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

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

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

// PzdcDoc snippet of: 'ru.bgcrm.struts.action.ProcessAction', lines: 1254 - 1258

// проверка и обновление статуса вкладки, если нужно
IfaceState ifaceState = new IfaceState(form);
IfaceState currentState = new IfaceState(Process.OBJECT_TYPE, id, form, String.valueOf(searchResultLink.getPage().getRecordCount()),
        String.valueOf(searchResultLinked.getPage().getRecordCount()));
new IfaceStateDAO(con).compareAndUpdateState(ifaceState, currentState, form);

Отображение в JSP:

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

<c:if test="${processType.properties.configMap['processShowProcessLinks'] eq '1'}">
    <c:set var="ifaceId" value="link_process"/>
    <c:set var="ifaceState" value="${ifaceStateMap[ifaceId]}"/>

        <c:url var="url" value="process.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>

JSP

JS

Плагины

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

В каких местах производится правка для плагина:

  • XML объявление плагина plugin/name.xml, там же объявляются точки расширений.

  • Таблицы БД при необходимости создаются и модифицируются в build/update/patch.sql

  • Java код плагина в пакете ru.bgerp.plugin.name

  • Java библиотеки подключать в build.gradle после комментария: "библиотеки, попадающие в сборку BGERP"

  • JS код плагина в файле webapps/js/name.js, подключается через точку расширения в XML объявлении.

  • Actions плагина в файле webapps/WEB-INF/struts-config-name.xml

  • Действия из обычного интерфейса должны быть объявлены в action/plugin.xml для контроля прав.

  • JSP плагина в webapps/WEB-INF/jspf/user/plugin/name

  • Для добавления плагина в сборку править build/update/build.xml

XML декларация

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

<?xml version="1.0" encoding="UTF-8"?>

<plugin package="ru.bgerp.plugin.blow">
        <endpoint id="user.menu.items.jsp" file="/WEB-INF/jspf/user/plugin/blow/menu_items.jsp"/>
        <endpoint id="js" file="/js/pl.blow.js"/>
        <endpoint id="css.jsp" file="/css/style.blow.css.jsp"/>
        <endpoint id="open.jsp" file="/WEB-INF/jspf/open/plugin/blow/url.jsp"/>
</plugin>

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

  • JSP шаблоны;

  • JS файлы;

  • package - пакет плагина, в котором должен быть размещён главный класс.

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

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

// PzdcDoc snippet of: 'ru.bgcrm.plugin.slack.Plugin', lines: 14 - 20

public Plugin(Document doc) {
        super(doc, ID);
        
        EventProcessor.subscribe((DefaultProcessorChangeContextEvent e, ConnectionSet conSet) -> {
                e.getContext().put(ID, new DefaultProcessorFunctions());
        }, DefaultProcessorChangeContextEvent.class );
}

Локализация

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

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

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

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

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

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

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

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

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

Запуск тестов

Unit тесты

./gradlew test

Интеграционный тест создания БД

Файл для установки должен быть собран предварительно командой.

./gradlew buildErp

Команда делает вызов Ant сценария в build/bgerp, передавая классы, скомпилированные Gradle.

Интеграционный тест, устанавливающий приложение и заполняющий тестовую базу. Только под Linux либо WSL. sudo необходим для установки в стандартный /opt каталог.

Тест пересоздаст базу с именем bgerp и переустановит сервер в /opt/BGERP
sudo GRADLE_OPTS=-Xmx1000m ./gradlew -Ptest.single='ru.bgerp.itest.RunServerTest' -Pdb.host=DB_HOST -Pdb.user=ROOT_USER -Pdb.pswd=ROOT_PSWD integrationTest

Параметры DB_HOST, ROOT_USER, ROOT_PSWD - для доступа к MySQL серверу, где будет создана тестовая БД.

Selenium e-to-e тест

Может выполнять впоследствия действия на запущенном сервере. Необходима установка chromedriver и указание его в пути.

GRADLE_OPTS=-Xmx1000m ./gradlew -Ptest.single='ru.bgerp.itest.SeleniumTest' -Pwebdriver.chrome.driver=/usr/lib/chromium-browser/chromedriver integrationTest

Методика разработки для клиента

BGERP запускается в IDE, с подключением к удалённой базе и биллингу (при необходимости). По окончании разработки удалённая база клиента наполнена актуальной конфигурацией и необходимо только обновить сам продукт.

  • Подключаться к клиенту по SSH, пробрасывая соединение к БД и при необходимости к биллингу. Пример: ssh user@X.X.X.X -L3307:127.0.0.1:3306 -L8081:Y.Y.Y.Y:8080

  • Создать свой bgerp_customer.properties файл, в нём можно прописывать параметры доступа и конфигурацию. Он не сохранится в GIT. В нём же можно переопределить URL для подключения к биллингу на locahost.

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

  • Создать конфигурацию запуска в IDE с данным properties.

  • Можно поправить .gitignore для сохранения custom файлов клиента.