Контекстные переменные

Назначение

Модуль контекстных переменных превращает данные одного объекта ERP в именованный строковый контекст. Например, для процесса 127 ключ process:title содержит название процесса, а process:param5503 — отображаемое значение его параметра 5503.

Контекст используется при формировании уведомлений, новостей и документов, а также доступен через HTTP API для редакторов, которым нужен список переменных и их значения.

У модуля нет отдельного административного экрана. Пользователь встречает контекстные переменные внутри текстовых полей других модулей. Каталог для таких редакторов отдаёт HTTP API.

Не следует смешивать этот механизм с двумя похожими возможностями:

  • JEXL исполняет выражение и может вернуть объект произвольного типа;

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

  • этот модуль отдаёт только именованные строковые значения и подставляет их в текст по ключу.

Состав модуля

Компонент Пакет или класс Назначение

Фабрика контекста

ContextVariableBuilderFactory

Строит Map<String, Supplier<String>> по типу и ID объекта

Каталог

ContextVariableCatalog

Отдаёт названия постоянных переменных, префикс параметров и тип параметризуемого объекта

Билдеры объектов

ru.bgcrm.core.contextVariable.model

Загружают объект, объявляют постоянные переменные и добавляют заполненные параметры

Запросы каталога и значений

ContextVariableCatalogQuery, ContextVariableValuesQuery

Готовят данные для HTTP API и фронтового репозитория

Шаблонизатор

TextTemplateProcessor

Заменяет конструкции {{ ключ }} значениями контекста

Комбинированный контекст

CombinedContextVariableBuildHelper

Дополняет процесс данными первого связанного контрагента и договоров

Сервисы зарегистрированы в глобальном ServerContext:

val factory = ServerContext.lookup(ContextVariableBuilderFactory::class.java)
val catalog = ServerContext.lookup(ContextVariableCatalog::class.java)
val templates = ServerContext.lookup(TextTemplateProcessor::class.java)

Построение контекста

Основной контракт:

interface ContextVariableBuilderFactory : Service {
    fun buildContext(
        objectId: Int,
        objectType: String
    ): Map<String, Supplier<String>>
}

Вызов buildContext выполняет следующие шаги:

  1. Проверяет, что objectType не пуст.

  2. Находит зарегистрированный билдер типа объекта и создаёт новый экземпляр.

  3. Загружает объект с ERP_MASTER.

  4. Создаёт поставщики постоянных переменных из объявлений билдера.

  5. Находит заполненные параметры объекта, оставляет только параметры его типа и добавляет их с префиксом <тип>:param.

  6. Публикует ContextVariablesBuildEvent и добавляет переменные слушателей.

Значение возвращается как Supplier<String>, но это не делает всю операцию ленивой: объект и его параметры уже прочитаны при buildContext. Отложено только получение строки из загруженной модели. Поэтому не следует собирать один и тот же контекст в цикле или отдельно для каждого ключа.

val context = factory.buildContext(127, "process")

val id = context.getValue("process:id").get()
val title = context.getValue("process:title").get()
val address = context["process:param5503"]?.get().orEmpty()

Пустые значения модели и параметров превращаются в пустую строку. Если объект известного типа не найден, билдер бросает BGMessageException с названием типа и ID.

Неизвестный тип не считается ошибкой: фабрика пропускает шаги билдера, публикует событие и возвращает только переменные слушателей. Если слушателей нет, результатом будет пустая карта.

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

Полный список типов и ключей приведён в каталоге контекстных переменных.

Параметры объектов

Для параметра с ID 5503 ключ строится как <префикс>param5503, например process:param5503 или customer:param5503. Значением служит отображаемое значение ParameterValuePair.getValueTitle(), а не внутреннее значение из БД.

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

  • параметры, объявленные для типа объекта;

  • параметры, у которых есть значение у конкретного объекта.

Каталог работает иначе: он должен показать пользователю доступный выбор заранее, поэтому включает все параметры типа. Для процесса список ограничивается параметрами указанного типа процесса, для остальных объектов берётся весь список их типа параметров.

Каталог

ContextVariableCatalog отделён от фабрики контекста, потому что ContextVariableBuilderFactory реализуется в том числе Java-кодом и изменение его интерфейса ломает бинарную совместимость.

interface ContextVariableCatalog : Service {
    fun catalog(objectType: String): Map<String, String>

    fun parameterCatalog(
        objectType: String,
        parameterIds: Collection<Int>
    ): Map<String, String>

    fun parameterObjectType(objectType: String): String?
}

catalog возвращает постоянные переменные в формате ключ → название. parameterCatalog строит названия выбранных параметров, а parameterObjectType сообщает, из какого типа объекта читать определения параметров. Последнее важно для коротких внешних имён house, street и city, чьи параметры зарегистрированы под типами address_house, address_street и address_city.

Обычно фронту и прикладному коду не нужно вызывать эти методы по отдельности: ContextVariableCatalogQuery объединяет постоянные переменные и параметры в один список.

Комбинированный контекст

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

  1. контекст самого процесса;

  2. контекст первого связанного контрагента;

  3. контекст первого связанного единого договора;

  4. контекст первого связанного договора биллинга с типом contract:<billingId>.

Для единого договора помощник добавляет первого связанного контрагента. Для остальных типов объектов он возвращает обычный контекст фабрики.

val context = CombinedContextVariableBuildHelper()
    .buildContext(processId, Process.OBJECT_TYPE)

val processTitle = context["process:title"]?.get().orEmpty()
val customerTitle = context["customer:title"]?.get().orEmpty()

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

Тип contract:<billingId> не имеет штатного билдера ядра. Его значения появляются только если этот тип зарегистрирован внешним модулем или обработан слушателем события.

Расширение

Новый тип объекта

Для нового типа создайте наследника ContextVariableBuilder<T>. Одно объявление ContextVariableDefinition одновременно задаёт ключ, название в каталоге и способ получения значения.

class OrderContextVariableBuilder(
    private val orderService: OrderService = ServerContext.lookup(OrderService::class.java)
) : ContextVariableBuilder<Order>() {

    override val objectType = "order"
    override val paramPrefix = "order:param"

    override val definitions = listOf(
        ContextVariableDefinition<Order>("order:id", "Заказ: ID") {
            it.id.toString()
        },
        ContextVariableDefinition("order:title", "Заказ: название") {
            it.title
        }
    )

    override fun load(objectId: Int): Order? = orderService.find(objectId)

    override fun notFoundMessage(objectId: Int) = "Заказ #$objectId не найден"
}

DefaultContextVariableBuilderFactory.registerFactory(
    "order",
    OrderContextVariableBuilder::class.java
)

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

Новые переменные существующего типа

Динамический код расширяет контекст событием ContextVariablesBuildEvent. Для этого слушатель подписывается на событие и помечает метод аннотацией @OnContextVariablesBuild.

public final class ProcessContextVariableListener extends AnnotatedEventListener {
    public ProcessContextVariableListener() {
        EventProcessor.subscribe(this, ContextVariablesBuildEvent.class);
    }

    @OnContextVariablesBuild(objectType = Process.OBJECT_TYPE)
    public void build(ContextVariablesBuildEvent event) {
        event.getContext().put(
            "process:custom",
            () -> computeValue(event.getObjectId())
        );
    }
}

objectType в аннотации — регулярное выражение, которое должно совпасть со всем именем типа. Значение по умолчанию .* принимает все типы.

Для процесса событие выполняется также через класс «Скрипт» его типа процесса. Имя класса берётся из ProcessTypeProperties.getActualScriptName(). Так конкретный тип процесса может добавлять свои переменные без изменения ядра.

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

Где используется

Основные потребители модуля:

Конкретный потребитель может использовать обычный либо комбинированный контекст. Наличие customer:* рядом с process:* поэтому зависит не от фабрики процесса, а от выбранного потребителем помощника.

Ограничения

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

  • Все штатные билдеры читают данные с ERP_MASTER.

  • Значения строковые; типы чисел, дат и коллекций теряются.

  • Неизвестный ключ при шаблонизации превращается в пустую строку и может скрыть опечатку.

  • Переменные события могут заменить штатные ключи и не видны в каталоге.

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