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

Оставь место стоянки чище, чем оно было до твоего прихода.

Редактируя тот или иной блок кода необходимо уменьшать использование устаревших подходов, методов, классов. Писать новый код, используя последние рекомендации и параллельно корректировать смежный код. При тестировании изменения будут одновременно проверены и улучшения кода. Таким образом постепенно качество кода будет расти. В дальнейшем будут внедрены формальные метрики качества кода: количество предупреждений компилятора, проверки формата и т.п.

Проект

Среда разработки

The program may be developed at any Java supporting platform:

  • Windows - this time the maintainer uses it;

  • Linux;

  • MacOS.

Minimal required set of software:

Console environment

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

Система сборки и публикации реализована на bash скриптах, для Windows их можно выполнять с помощью:

  • WSL - встроенная в современные Windows виртуальная Linux машина, предпочтительный путь;

  • Cygwin - Windows порты *NIX утилит.

Как удобная консольная оболочка под OS Windows рекомендуется ConEmu со встроенным FAR Manager.

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

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

ide format

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

При приведении в норму формата файлов следует учитывать следующие правила:

  1. Все новые файлы должны быть корректно форматированы.

  2. Ни в коем случае не применять оба символа отступов в одном файле: табуляторы и пробелы. Файл в таком случае становится нечитаемым в некоторых редакторах. При обнаружении подобных случаев - приводить все отступы в файле к рекомендуемому формату.

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

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 Developers, эта сборка уже содержит GIT клиент, редакторы JSP и XML.

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

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

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

Window - Preferences - Team - Git - History снять галочку Relative history

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

VS Code

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

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

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

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

Running in IDE

Configuration:

  • Main class: ru.bgerp.Server

  • Program arguments: start

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

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

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

Database

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

For database creation use sequentially the files:

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

Execute mysql commands:

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

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

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

Файлы

Перечень каталогов проекта с описанием.

Библиотеки

Java

При запуске программы подключаются JAR файлы из следующих каталогов:

Сторонние библиотеки поставляются в отдельном пакете обновления. Артефакты и версии указываются в файле build.gradle для конфигурации bgerp.

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

Для IDE Eclipse для ускорения изначальной инициализации проекта по-умолчанию отключена загрузка исходных кодов и документации.

// PzdcDoc snippet of: 'build.gradle', lines: 18 - 24

eclipse {
    classpath {
        // при необходимости в документации или исходных кодах библиотек - закомментировать две строки ниже и вызвать "Refresh Gradle Project"
        downloadJavadoc = false
        downloadSources = false
    }
}
JS

JS библиотеки располагаются в следующих каталогах:

  • webapps/js - располагаются JS файлы самого приложения: ядра и плагинов;

  • webapps/lib - сторонние библиотеки.

Для упрощения отладки используются не минифицированные версии библиотек.

Некоторые сторонние библиотеки пропатчены, все изменения сопровождены комментариями, пример:

// 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
                : '');

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

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 файлов клиента, если работа происходит в форке.

GIT Workflow

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

Ветка Базируется на Назначение 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
git commit -am "pXXXXX Some change description."
git push

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

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

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

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

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

Публикация основной документации производится с ветки ветке p11862-documentation. Для предложения правок документации следует создать ветку, начинающуюся с данной.

Локализация

Ветки, содержащие только локализацию интерфейса, следует начинать с долгоживущей ветки p12136-localization.

Архитектура

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.

БД

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

Логика Java Actions

Action классы определяются в struts-config*.xml файлах, и привязываются к URL запроса. Имя метода передаётся в HTTP параметре запроса action. Если параметр отсутствует, то вызывается unspecified метод, подобный способ нежелателен.

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

<!-- 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, методы возвращать результат через вызов data либо status. Первый метод обрабатывается JSP страницей-форвардом и высылает HTML на клиент. Второй - предназначен для выполнения изменений и возвращает только JSON формат с результатом выполнения.

В каждый вызов акшена передаётся супер объект form 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 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");
}

Именование

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

  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: 66 - 69 -->

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

Проверка прав

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

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

<!-- PzdcDoc snippet of: 'plugin/action/kernel.xml', lines: 151 - 152 -->

<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[]. Второй класс удобен возможностью склеивать как запросы, так и параметры и не указывать номер позиции устанавливаемых параметров.

Кэши

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

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

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

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

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

        UserCache.flush(con);

        return status(con, form);
    }

Представление JSP

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

Теги

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

ide jsp tag

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

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

Функции

JSP функции объявлены в каталоге webapps/WEB-INF/tld. Также как и для тегов для IDE поддерживает автодополнение. Используются, например, для форматирования выводимых времён.

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

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

Сохранение контекста

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

JS (Java Script)

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

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

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

// PzdcDoc snippet of: 'webapps/js/kernel.ajax.js', lines: 350 - 355

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

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

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

Плагины

Всякая логически обособленная функциональность должна быть вынесена в плагин. Особенностью плагинов является их изолированность. Каждый плагин работает с ядром, ядро не знает особенностей конкретного плагина, плагины не знают друг о друге. Под знанием тут подразумевается полагание на конкретное 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 );
}

Пункты меню

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

<!-- PzdcDoc snippet of: 'plugin/blow.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: 'plugin/blow.xml', lines: 5 - 5 -->

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

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

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

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

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

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

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

diag fa60097de6559303acd31dfacc9aab84

Локализация

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

Локализация отдельно от изменения может быть выполнена в отдельной ветке GIT.

Для отключения в момент разработки кэширования локализаций установите в конфигурации 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>

Локализация может выполняться в 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: 205 - 205

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

Именование

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

    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: 241 - 253

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: 221 - 228

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

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

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

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

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

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

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

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

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

В JSP - функция u:formatDate():

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

        <td>${u:formatDate(item.scheduledTime, 'ymdhms')}</td>
        <td>${u:formatDate(item.executedTime, 'ymdhms')}</td>

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

Для ускорения парсинга и валидации используются 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')}"/> 

Логирование

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: 56 - 64 --%>

<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: 212 - 214

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

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.childs}">
        <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: 139 - 158 --%>

<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}">Cозданные мной</li>
                                <li value="${MODE_USER_CLOSED}">Закрытые мной</li>
                                <li value="${MODE_USER_STATUS_CHANGED}">Статус изменён мной</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="Вывести">
                                <%-- &#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 () {
                openUrlContent("/user/log.do");
        });
    }); 
</script>

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

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

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

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 update@bgerp.org. Before you publish a change update, make sure, that ssh update@bgerp.org session works by 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.

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

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

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

bash -c "./gradlew buildClean buildUpdate"

либо:

bash -c "./gradlew buildClean buildUpdateLib buildUpdate"

Далее:

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

Merge the latest state of the master on documentation branch.

Build Docker image.

Docker

docker login --username bgerp

Input access token.

Go in directory build/docker

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

Unit тесты

Используется фреймворк JUnit, каталог srcx/test. Проверка отдельных алгоритмов, тесты не зависят друг от друга, не работают с БД.

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

bash -c "./gradlew clean test"

Интеграционный тест

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

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

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

bash -c "./gradlew buildErp"

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

GitLab CI

В файле .gitlab-ci.yml настроен автоматический запуск задач на каждый коммит в GIT. Различные задачи выполняются в разных ветках Workflow. Ниже описание CI задач.

test-integration

publish-doc

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

publish-source

Публикует актуальные исходные коды из master в открытый репозитарий http://git.pzdc.de/pub/bgerp/bgerp/. Данный способ выбран для уменьшения размера открытого репозитария и сокрытия удалённых ранее файлов из истории.