/SysConfigFile

Class for Dynamics AX 2009, 2012 and AX4

Primary LanguageJavaMIT LicenseMIT

SysConfigFile 2.1

SysConfigFile – это класс, который позволяет получать значения параметров из конфигурационных xml-файлов в Microsoft Dynamics AX 2009, Microsoft Dynamics AX 2012 и Axapta 4.0.

Как правило, на проектах используется несколько экземпляров (инстансов) Аксапты - рабочая, тестовая, для разработки и т.д. Причем зачастую рабочий экземпляр полностью копируется в один из тестовых экземпляров (и база, и приложение). Кроме того, зачастую создается экземпляр-зеркало рабочего экземпляра. Зеркало используется для BI-отчетов.

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

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

Класс SysConfigFile

Класс SysConfigFile предполагает:

  1. параметры хранятся централизовано на сервере в небольших xml-файлах с расширением .config или .config.xml
  2. имя config-файла по умолчанию Axapta (имя можно задать в new или в конструкторе)
  3. каталог по умолчанию %Appl%\Config (каталог можно задать в new или в конструкторе, изменить можно в parm-методе)
  4. параметр хранится как xml-элемент, а значение параметра хранится как текстовое значение xml-элемента. Например, <name>Microsoft Dynamics AX</name>
  5. пустые xml-элементы трактуются как true методом getBoolean и как пустое значение методом get. См. Пример config-файла
  6. валидность config-файл можно проверить при помощи xsd-схемы, которую можно централизовано хранить в ресурсах Аксапты или в xsd-файле рядом с config-файлом. Схему также можно явно указать в конструкторе

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

Формат XML сам по себе не ограничивает уникальность элементов внутри, поэтому класс SysConfigFile позволяет получать как одно найденное значение (первое), так и список всех найденных значений. Кроме того, метод getBy позволяет получить значения в нужном порядке, а не в порядке размещения в xml-файле. См. также раздел Направления для развития.

Класс SysConfigFile будет работать даже в том случае, если config-файл отсутствует. В этом случае get-методы будут возвращать значения по умолчанию. Однако класс позволят программисту явно сказать, что параметр обязательно должен присутствовать в файле при помощи assert-, ensure- и check- методов. Обратите внимание, что статический метод ::Value бросает исключение, а статический метод ::ValueOrDefault возвращает значение по умолчанию, если параметра нет в конфигурационном файле. См. Устойчивость.

Для работы с XML-файлами класс SysConfigFile пока использует XmlDocument. XmlDocument позволяет искать элементы внутри этого файла при помощи XPath, но загружает xml-файл целиком в память. Поэтому следите за размером config-файла, не позволяйте этому файлу стать слишком большим. См. Несколько разных config-файлов и Кэширование.

Пример config-файла

<?xml version="1.0" encoding="utf-8"?>
<config>
    <id>PROD</id>
    <name>Microsoft Dynamics AX</name>
    <reportTemplateFolder>\\dax\template\</reportTemplateFolder>

    <sender>Axapta</sender>
    <sender email="note">Notification server</sender>
    <sender email="mail">Company name</sender>

    <AOS>
        <batch serverId="01@AOS" />
        <batch serverId="01@RESERV">true</batch>
    </AOS>
</config>

Пример X++

Предположим, что в каталоге %Appl%\Config\ содержится файл Axapta.config с текстом из примера выше. Тогда код будет работать так (см. тестовый метод SysConfigFileTest.testExample):

SysConfigFile config = SysConfigFile::construct();

config.get('id');                       // 'PROD'
config.get('notFound');                 // ''
config.ensureExists('notFound').get();  // бросит исключение, поскольку
                                        // в данном примере параметр notFound отсутствует

config.getBoolean('AOS/batch');         // true
config.getBoolean('notFound');          // false

config.getAll('sender');                // ['Axapta','Notification server','Company name']
config.getAll('AOS/batch/@serverId');   // ['01@AOS','01@RESERV']
config.getAll('notFound');              // connull()

config.getBy(['sender[@email="mail"]','sender[not(@email)]']);  // 'Company name'
config.getBy(['sender[@email="other"]','sender[not(@email)]']); // 'Axapta'

Если нужно получить только одно значение, то можно написать и так:

SysConfigFile::value('name');           // 'Microsoft Dynamics AX'

Два статических метода Value и ValueOrDefault по разному работают с отсутствующими в config-файле параметрами:

SysConfigFile::valueOrDefault('notFound','DefaultValue');       // 'DefaultValue'

SysConfigFile::value('notFound');                               // бросит исключение

Чтобы проверить семантику config-файла, программисту достаточно вызвать метод ensureFileValid один раз:

SysConfigFile config = SysConfigFile::construct().ensureFileValid();

config.get('id');                       // 'PROD'
config.getBoolean('AOS/batch');         // true

Несколько разных config-файлов

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

Если параметры хранятся в отдельном файле PrintSrv.config, то достаточно в конструкторе или в статическом методе написать:

SysConfigFile::construct('PrintSrv').get('id');

SysConfigFile::value('id', 'PrintSrv');

При желании можно указать и каталог, где хранится конфиг (см. тестирующий класс SysConfigFileTest). По умолчанию класс использует каталог Config внутри каталога приложения. К каталогу приложения %Appl% гарантировано имеют доступ все AOS кластера. Если админы не запретили специально доступ к каталогам внутри %Appl%, то к подкаталогу %Appl%\Config доступ скорее всего будет.

Расширения config-файлов

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

Если в Config-каталоге присутствует несколько конфигурационных файлов с одинаковым именем, но с разными расширениями, то будет возвращен первый существующий в указанном порядке: .config, .config.xml, .xml, пустое расширение.

Если ни один конфигурационный файл не найден, то, как и прежде, используется имя с расширением .config.

Вариант "без дополнительного расширения" позволяет программисту указать в наименовании конфига то имя файла, какое ему хочется, и не парится о соглашениях.

См. метод filePath()

Валидация

Проверка синтаксиса config-файла

Класс SysConfigFile использует аксаптовские классы для работы с xml. Эти классы всегда выполняют проверку xml синтаксиса при загрузке текста - так называемую проверку well formed xml. См.также W3 well-formed, статья

Валидация по xsd-схеме

Класс SysConfigFile может выполнить валидацию config-файла по xsd-схеме: https://www.w3.org/XML/Schema

Чтобы выполнить валидацию, программист должен явно вызвать хотя бы один из методов: ensureFileValid, assertFileValid или checkFileValid. Если методы не вызваны, то класс выполняет только проверку синтаксиса при чтении config-файла.

Xsd-схема может хранится централизовано в ресурсах AOT или в xsd-файле рядом с config-файлом, а может задаваться как параметр при создании класса. Класс определяет название ресурса в методе schemaResourceName, а название и путь xsd-файла в методе schemaPath.

Валидация по dtd не выполняется

Аксаптовские xml-классы не выполняет валидацию по DTD (выполняется только проверка синтаксиса DTD как подмножества синтаксиса xml).

С некоторого времени Microsoft считает работу с DTD опасной: https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.

Во-первых, из-за "xml bomb" (гуглите).

Во-вторых, потому что способ проверки задает создатель файла внутри этого xml-файла. В далекие 2000е в этом не видели проблему, но уже в 2010е стали считать, что доверять создателю файла слишком наивно и опасно. Поэтому, в аксаптовских xml-классах есть метод prohibitDtd. По умолчанию он возвращает true.

У меня не получилось включить процессинг DTD в аксапте даже передав false в метод prohibitDtd. Дайте знать, если у вас получится.

RunOn = Server

SysConfigFile – серверный класс (параметр RunOn = Server в свойствах AOT). Так сделано для того, чтобы:

  • класс гарантировано читал config-файл с сервера
  • не нужно было расшаривать каталог с конфигами в сети (в том числе, VPN)
  • не путаться с путями к файлами (на клиенте, локально на сервере, сетевые пути)
  • не беспокоиться о правах доступа пользователей и AOSов к конфигам (особенно в кластере)

Но это значит, что вызов этого класса из Аксаптовского клиента приведет к потенциально длительному клиент-серверному взаимодействию. Постарайтесь работать с этим классом из серверных объектов и методов.

Чтобы уменьшить накладные расходы на передачу данных между клиентом и сервером, метод getAll возвращает container, а не составной объект. По этой же причине метод getBy принимает container как параметр.

Если же Аксаптовскому клиенту нужно получить значения большого количества параметров, то создайте дополнительный специализированный метод, который упакует передаваемые данные в container. См. также Направления для развития.

Кэширование

Класс кэширует и найденные, и отсутствующие значения в глобальном кэше. Поэтому повторный get возвратит значение очень быстро, без дополнительного чтения config-файла.

Поскольку SysConfigFile – это серверный класс, то он будет использовать серверный глобальный кэш. В ax4 и ax2009 в методе globalCache я предлагаю использовать appl.globalCache. В ax2012 я предлагаю использовать classFactory.globalObjectCache. На своем проекте вы можете изменить код и использовать кэши из appl, infolog или classFactory.

Класс SysConfigFile может работать в startup процедурах, когда глобальные переменные appl, info, classFactory еще не инициализированы. На этом этапе класс создает свой локальный кэш, который будет жить пока живет объект SysConfigFile.

Кроме того, каждый объект класса SysConfigFile физически читает config-файл только один раз при необходимости, а потом хранит xmlRoot пока живет. Поэтому, если вам нужно получить несколько значений из конфига, старайтесь вызывать методы get, а не статические методы ::value. Это минимизирует число физических чтений config-файла.

SysConfigFile - неизменяемый объект

Класс SysConfigFile создает неизменяемые (Immutable) объекты – все значимые параметры определяются при создании объекта, хранятся в переменных объекта и не изменяются пока объект "живет".

Некоторые внутренние переменные класса вычисляют свое значение при необходимости (Lazy initialization) и не меняют значение после вычисления.

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

Прямой доступ к xml-содержимому

Программист может получить исходный текст конфига и исходный текст xsd-схемы при помощи методов file() и schema().

Кроме того, класс содержит приватные методы xmlDocument(), xmlRoot() и xmlSchema(), которые возвращают ссылки на объекты в памяти. Если вы хотите получить непосредственный доступ к xml-содержимому, то сделайте эти методы публичными и работайте с содержимым xml.

Однако помните, что SysConfigFile – серверный. Не обращайтесь к методам xmlDocument() и xmlSchema() из клиента – клиент-серверная передача отдельных объектов сопровождается огромными накладными расходами.

Также учитывайте, что в следующих версиях класс SysConfigFile может использовать другие способы работы с xml.

Устойчивость

Класс SysConfigFile разработан так, чтобы возвращать осмысленный результат в ситуациях, которые могут трактоваться как ошибки. Класс минимизирует вероятность возникновения исключений и в явном виде бросает исключения только в ensure- методах. Однако само ядро Аксапты может выбросить исключение (exception) во время парсинга xml в методах XmlDocument::newFile() и XmlSchema::newFile() если:

  • xml-файл или xsd-схема содержит незакрытые теги, неправильно вложенные теги, ", &, <, > или другие специальные символы
  • xsd-схема содержит непредусмотренные стандартом элементы, атрибуты или значения

Обработка исключений в классических Аксаптах имеет особенность: catch блоки внутри транзакции не обрабатываются, Аксапта выпрыгивает за самый внешний transaction-блок и ищет catch уже там (см. раздел Exception handling inside transaction в документации). Поэтому обработка исключений становится не надежной, если код может выполняться внутри транзакции.

Поэтому если config-файлы содержат синтаксически правильный xml, то исключения могут возникнуть только после вызова ensure-, assert- методов и статического метода ::Value, а остальные методы вернут более-менее осмысленный результат.

Зависимости

В версии 2.0 класс SysConfigFile использует только стандартный функционал Аксапты и несколько .net методов, которые обернуты в необходимые CodePermission. Поэтому вам не придется ни импортировать дополнительные проекты, ни устанавливать дополнительные CodePermissions в свой код.

Класс SysConfigFile использует:

  • System.IO.File::Exists
  • System.String::Copy
  • System.String.Trim

Тестовый класс SysConfigFileTest использует:

  • System.IO.Path::GetTempPath
  • System.IO.Directory::CreateDirectory
  • System.IO.Directory::Delete

Направления для развития

  • использовать .net классы для работы с xml вместо Аксаптовских (проверить производительность, потребление/утечку памяти и работу .net GC в связке с GC-Аксапты разных версий)
  • дальнейшая минимизация трафика между клиентом и сервером Аксапты: принять несколько параметров в контейнере и возвратить контейнер значений (развить метод getAll)
    • возможно, стоит вернуть упакованный map
  • добавить getDate, getTime, getDateTime, которые будут умно конвертировать строку формата, использующийся xsd-схемами xs:date, xs:time, xs:dateTime, в Аксаптовские объекты с типом Date, Time и DateTime соответственно
    • Насколько я понимаю, это один из вариантов формата ISO 8601: yyyy-mm-ddThh:mm
    • Подумать в каком методе и как правильно учесть часовой пояс yyyy-mm-ddThh:mmZ### (а как для ax4?)
    • Подумать стоит ли делать три метода или один (а как для ax4?)
    • см. также Global::xmlstring
  • добавить работу с namespace в config-файле (нужно ли?)
  • сделать класс, который сможет читать параметры из реестра, а не из файла. В этом случае администраторы смогут раскатывать значения по серверам через групповые политики

Известные проблемы

1. XML чувствителен к регистру в запросе, кэш не чувствителен к регистру

Эта проблема проявляется очень редко только в ax4 и ax2009 и не особо мешает использовать данный (в целом полезный) класс. Используемый в ax2012 класс SysGlobalObjectCache работает с регистрозависимыми ключами и не имеет данной проблемы. Поэтому я решил опубликовать проект с этой проблемой. Дайте знать, если у вас есть предложения по данному пункту.

Прежде всего, привыкшие к Аксапте программисты постоянно забывают о регистрозависимости XML. И постоянно делают попытки получить name вперемешку с Name. А это разные элементы с точки зрения XML.

На регистрозависимый XML накладывается Аксаптовский НЕ зависимый от регистра кэш SysGlobalCache. Поэтому с этим кэшем следующие строки кода будут вести себя по разному (внезапно!):

SysConfigFile::value('Name');   // бросит исключение, поскольку xPath запрос не найдет такого раздела в XML-файле
SysConfigFile::value('name');
SysConfigFile::value('name');   // 'Microsoft Dynamics AX'
SysConfigFile::value('Name');   // 'Microsoft Dynamics AX', а не исключение! поскольку параметр успешно найден в кэше

2. Хочется работать с config-файлами на клиенте

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

SysConfigFile – серверный класс. Это фича. Но иногда нужно работать с config-файлами на клиенте (POS-терминалы, PinPad, ключи безопасности и прочее оборудование, сертификаты и софт, привязанный к пользовательскому компьютеру). Конечно, с клиентскими config-файлами можно работать как с обычными xml-файлами. Если же хочется получить сервис SysConfigFile на клиенте, то можно:

  • создать дубль SysConfigFileClient
  • изменить свойство RunOn
  • изменить WinAPIServer на WinAPI
  • изменить метод parmDefaultDirectory или явно указывать каталог перед запросом значений из конфига

Благодарности

  • Спасибо Сергею Чечкину за похоже единственно возможный в аксаптовских классах способ верификации xml по схеме
  • Спасибо всем за конструктивное обсуждение проекта

Disclaimer

  • Названия классов и методов, иерархия и порядок вызовов в наборе классов будут по возможности сохраняться, но это не гарантируется - в будущих версиях SysConfigFile все может измениться.
  • Код в xpp-файлах конвертирован из xpo только для удобства использования человеком. Оригиналом является код в xpo-проектах, отличия между xpo и xpp всегда трактуются в пользу текста из xpo-проектов.
  • Проект выложен "как есть" под лицензией MIT: вы можете использовать данный код как угодно безо всяких отчислений, автор не дает никаких гарантий и не несет ответственности за возможный эффект от использования кода на проектах.

Прочее

  • проект сознательно сделан для классических версий Аксапты
  • в проекте сознательно не используется xmldocs
  • README и комментарии сознательно сделаны на русском языке
  • ошибки в ensure методах записаны простой строкой на русском языке и не используют меток

ChangeLog

Помощь проекту

Буду признателен за ваши замечания, предложения и советы по проекту как в разделе Issues, так и в виде письма на адрес mazzy@mazzy.ru

Мазуркин Сергей (mazzy)