Введение
При работе с ориентированным на данные XML часто требуется оперировать "управляемыми словарями", известными также как перечисляемые величины. Давайте рассмотрим следующий пример банковского обобщённого счёта:
<accountSummary>
<timestamp>2003-01-01T12:25:00</timestamp>
<currency>USD</currency>
<balance>2703.35</balance>
<interest rounding="down">27.55</interest>
</accountSummary>
В этом документе присутствуют два управляемых словаря. Первый - это словарь валют, трехсимвольный код валюты по ISO-4217 ("USD" - это доллар США). Второй - это правило округления (rounding) процентов: "up" (в сторону увеличения), "down" (в сторону уменьшения) и or "nearest" (ближайший). В нашем примере банк предпочитает округлять проценты в сторону уменьшения.
Проблема, возникающая при проектировании этой схемы, заключается в том, что управление кодами валют ISO осуществляется вне нашего ведома. Эти коды могут быть изменены в любой момент времени, и если их встроили в схему, ее потребуется переиздавать всякий раз, как организация ISO меняет свои коды. А это может оказаться весьма накладным. Сказанное особенно справедливо в отношении предприятия, где любая модификация схемы, какой бы незначительной она ни была, может потребовать проведения полного тестирования приложения, использующего схему.
В этой статье рассказывается, как можно управлять управляемыми словарями при использовании W3C XML-схемы (W3C XML Schemas), поскольку именно эта спецификация является основным форматом XML-схем для ориентированного на данные XML. Обратите внимание на то, что под "словарями" мы будем понимать перечисляемые списки значений элементов-атрибутов, что отлично от других контекстов, в которых "словари" - это наборы имен элементов XML.
Шаг №1: монолитная схема
Прежде чем задуматься над тем, какой из управляемых словарей находится вне нашего контроля, первое, что необходимо сделать, это создать схему для обобщённых счётов, используя W3C XML-схему. В рамках этой статьи мы воспользуемся подмножеством трехсимвольных кодов валют ISO. Соответствующая схема будет иметь следующий вид:
<xsd:schema xmlns:xsd = "http://www.w3.org/2001/XMLSchema"
version = "1.0"
elementFormDefault = "qualified">
<xsd:element name = "accountSummary">
<xsd:complexType>
<xsd:sequence>
<xsd:element ref = "timestamp"/>
<xsd:element ref = "currency"/>
<xsd:element ref = "balance"/>
<xsd:element ref = "interest"/>
</xsd:sequence>
<xsd:attribute name = "version" use = "required">
<xsd:simpleType>
<xsd:restriction base = "xsd:string">
<xsd:pattern value = "[1-9]+[0-9]*\.[0-9]+"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name = "timestamp" type = "xsd:dateTime"/>
<xsd:element name = "currency" type = "iso3currency"/>
<xsd:element name = "balance" type = "xsd:decimal"/>
<xsd:element name = "interest">
<xsd:complexType>
<xsd:simpleContent>
<xsd:extension base = "xsd:decimal">
<xsd:attribute name = "rounding" use = "required"
type = "roundingDirection"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
</xsd:element>
<xsd:simpleType name = "iso3currency">
<xsd:annotation>
<xsd:documentation>ISO-4217 3-letter currency codes,
as defined at (трехсимвольные коды валют ISO-4217, как определено по следующему адресу)
http://www.bsi-global.com/Technical+Information/Publications/_Publications/tig90.xalter
or available from (или доступно по следующему адресу)
http://www.xe.com/iso4217.htm
Only a subset are defined here (Здесь определяются только подмножества).</xsd:documentation>
</xsd:annotation>
<xsd:restriction base = "xsd:string">
<xsd:enumeration value = "AUD"/><!-- Australian Dollar -->
<xsd:enumeration value = "BRL"/><!-- Brazilian Real -->
<xsd:enumeration value = "CAD"/><!-- Canadian Dollar -->
<xsd:enumeration value = "CNY"/><!-- Chinese Yen -->
<xsd:enumeration value = "EUR"/><!-- Euro -->
<xsd:enumeration value = "GBP"/><!-- British Pound -->
<xsd:enumeration value = "INR"/><!-- Indian Rupee -->
<xsd:enumeration value = "JPY"/><!-- Japanese Yen -->
<xsd:enumeration value = "RUR"/><!-- Russian Rouble -->
<xsd:enumeration value = "USD"/><!-- US Dollar -->
<xsd:length value = "3"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name = "roundingDirection">
<xsd:annotation>
<xsd:documentation>Whether the interest is
rounded up, down or to the
nearest round value.(Округляется ли процент вверх, вниз
или до ближайшего округленного числа.)</xsd:documentation>
</xsd:annotation>
<xsd:restriction base = "xsd:string">
<xsd:enumeration value = "up"/>
<xsd:enumeration value = "down"/>
<xsd:enumeration value = "nearest"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
Обратите внимание на два управляемых словаря (перечисления): простые типы iso3currency и roundingDirection - длина iso3currency явно установлена равной 3, чтобы избежать в будущем досадных опечаток при редактировании, когда потребуется корректировать список валют.
Также стоит отметить, что значению факультативного атрибута version было задано значение "1.0". Дело в том, что при работе с ориентированными на данные XML-сообщениями, часто необходимо одновременно поддерживать многочисленные версии схемы сообщений, поскольку, по-видимому, невозможно одновременно "поднять" системы, использующие эту схему, до ее последней версии. Таким образом, крайне необходимо указать версию схемы, по которой проверялось на допустимость XML-сообщение. С учетом сказанного, мы обозначим нашу схему accountSummary-1.0.xsd, чтобы будущие версии не перезаписали текущую.
Кроме того, для того, чтобы экземпляры сообщения четко идентифицировали свою версию схемы, к элементу accountSummary был добавлен атрибут version. Предполагается, что эти номера версии записываются как M.N, где M - это основной номер версии, а N - дополнительный. В результате, приведенный выше код для обобщённого счёта можно переписать следующим образом:
<accountSummary
version = "1.0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation = "accountSummary-1.0.xsd">
<timestamp>2003-01-01T12:25:00</timestamp>
<currency>USD</currency>
<balance>2703.35</balance>
<interest rounding = "down">27.55</interest>
</accountSummary>
Шаг №2: изолирование изменчивых управляемых словарей
При работе с управляемыми словарями (перечислениями) в схемах, важно оценить степень изменчивости каждого словаря. Изменчивый словарь - это словарь, изменения которого, как предполагается, не связаны с выходом версий схемы. Стабильный словарь - это словарь, который меняется (если такое вообще происходит), только если появляются новые редакции схемы. Если изменчивые словари включены в схему, то это чревато возникновением проблем, поскольку они требуют выпуска дополнительных версий всех зависимых приложений.
В нашем примере обобщённого счёта коды валют - это изменчивый словарь: они контролируются извне, международной организацией ISO, и ISO может добавлять или удалять валюты в любой момент времени. С другой стороны, набор правил округления {"up", "down", "nearest"}, вероятно, не будет меняться, поэтому он является стабильным словарем. Для человека, который осуществляет сопровождение приложения, оперирующего обобщёнными счётами, добавление нового правила округления означало бы написание, тестирование и внедрение новой версии этого приложения. Поэтому "политическое давление" привело бы к тому, что величины округления изменялись бы только как часть запланированного выхода редакции схемы. Таким образом, разумно оставить простой тип roundingDirection встроенным в эту схему.
Тем не менее, вряд ли было бы допустимо переписывать приложение, чтобы отрабатывать изменение в наборе кодов валют, в противном случае это свидетельствовало бы о негибкости разработки. Поскольку эти коды управляются извне, их необходимо изолировать: создадим для них отдельную схему словаря. Схема словаря - это схема, которая содержит единственное определение простого типа с перечисляемыми величинами и больше ничего. Такая схема, обозначенная как iso3currency-1.0.xsd, будет иметь следующий вид:
<xsd:schema xmlns:xsd = "http://www.w3.org/2001/XMLSchema"
version = "1.0"
elementFormDefault = "qualified">
<xsd:simpleType name = "iso3currency">
<xsd:annotation>
<xsd:documentation>ISO-4217 3-letter currency codes,
as defined at (трехсимвольные коды валют ISO-4217, как определено по следующему адресу)
http://www.bsi-global.com/Technical+Information/Publications/_Publications/tig90.xalter
or available from (или доступно по следующему адресу)
http://www.xe.com/iso4217.htm
Only a subset are defined here (Здесь определяются только подмножества).</xsd:documentation>
</xsd:annotation>
<xsd:restriction base = "xsd:string">
<xsd:enumeration value = "AUD"/><!-- Australian Dollar -->
<xsd:enumeration value = "BRL"/><!-- Brazilian Real -->
<xsd:enumeration value = "CAD"/><!-- Canadian Dollar -->
<xsd:enumeration value = "CNY"/><!-- Chinese Yen -->
<xsd:enumeration value = "EUR"/><!-- Euro -->
<xsd:enumeration value = "GBP"/><!-- British Pound -->
<xsd:enumeration value = "INR"/><!-- Indian Rupee -->
<xsd:enumeration value = "JPY"/><!-- Japanese Yen -->
<xsd:enumeration value = "RUR"/><!-- Russian Rouble -->
<xsd:enumeration value = "USD"/><!-- US Dollar -->
<xsd:length value = "3"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
Видно, словарь валют имеет свою собственную версию и, таким образом, период выхода очередной редакции. Эта схема словаря может быть включена в новую (1.1) версию основной схемы сообщения:
<xsd:schema xmlns:xsd = "http://www.w3.org/2001/XMLSchema"
version = "1.1"
elementFormDefault = "qualified">
<xsd:include schemaLocation = "iso3currency-1.0.xsd"/>
<xsd:element name = "accountSummary">
<xsd:complexType>
<xsd:sequence>
<xsd:element ref = "timestamp"/>
<xsd:element ref = "currency"/>
<xsd:element ref = "balance"/>
<xsd:element ref = "interest"/>
</xsd:sequence>
<xsd:attribute name = "version" use = "required">
<xsd:simpleType>
<xsd:restriction base = "xsd:string">
<xsd:pattern value = "[1-9]+[0-9]*\.[0-9]+"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name = "timestamp" type = "xsd:dateTime"/>
<xsd:element name = "currency" type = "iso3currency"/>
<xsd:element name = "balance" type = "xsd:decimal"/>
<xsd:element name = "interest">
<xsd:complexType>
<xsd:simpleContent>
<xsd:extension base = "xsd:decimal">
<xsd:attribute name = "rounding" use = "required" type = "roundingDirection"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
</xsd:element>
<xsd:simpleType name = "roundingDirection">
<xsd:annotation>
<xsd:documentation>Whether the interest is
rounded up, down or to the
nearest round value.</xsd:documentation>
</xsd:annotation>
<xsd:restriction base = "xsd:string">
<xsd:enumeration value = "up"/>
<xsd:enumeration value = "down"/>
<xsd:enumeration value = "nearest"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
В соответствии с правилом обозначения, эта схема названа accountSummary-1.1.xsd. Обратите внимание на отсутствие кодов валют в основной схеме.
Шаг №3: развязывание управляемых словарей
Сложность с accountSummary-1.1.xsd заключается в том, что она напрямую импортирует iso3currency-1.0.xsd. Так, при появлении новой версии схемы словаря валют ISO, по-прежнему требуется выпускать новую редакцию схемы обобщённого счёта. Необходим механизм развязывания версий схемы словаря от версий основной схемы. Простое решение - воспользоваться "проходной" схемой словаря, не имеющей версии:
<xsd:schema xmlns:xsd = "http://www.w3.org/2001/XMLSchema"
elementFormDefault = "qualified">
<xsd:include schemaLocation = "iso3currency-1.0.xsd"/>
</xsd:schema>
У этой схемы, обозначенной как iso3currency.xsd, отсутствует атрибут version. Для завершения развязывания схем выпускается новая версия основной схемы, accountSummary-1.2.xsd, - единственное ее отличие от версии 1.1 состоит в том, что элемент <xsd:include> изменился с
<xsd:include schemaLocation = "iso3currency-1.0.xsd"/>
на
<xsd:include schemaLocation = "iso3currency.xsd"/>
чтобы включить не имеющую версии схему словаря валют. Так выполняется развязывание схем. Если организация ISO изменяет список кодов валют, выходит новая схема валют и корректируется
iso3currency.xsd, чтобы она импортировала эту новую схему валют. Основная схема остается без изменений, поскольку она содержит
iso3currency.xsd и не зависит от версии схемы словаря валют.
Шаг №4: защита приложений
Подобное развязывание схем словарей не лишено сложностей. Во-первых, по мере того, как будут появляться новые версии схемы словарей валют, существующие файлы экземпляров сделаются недопустимыми, если в них содержатся коды валют, которые были удалены ISO. В некоторых случаях такая ситуация неприемлема, но для нашего примера это возможно. Если файл экземпляра ссылается на код валюты, который более не существует, он станет семантически недопустимым; ему также ничто не мешает оказаться синтаксически недопустимым. Тогда можно воспользоваться синтаксической недопустимостью, чтобы обнаруживать такие экземпляры и направлять их для специальной обработки, чтобы код основного приложения смог заниматься допустимыми кодами валют. Убрав обработку ошибок из основного приложения, можно сократить код основного приложения и упростить его сопровождение.
Во-вторых, с учетом того, что коды валют могут менять в любой момент, необходимо обеспечить синхронизацию между кодами валют в схеме словаря валют и кодами валют, известным приложениям. Эту задачу можно решить двумя способами. В первом случае приложения могут воспользоваться схемой словаря как источником кодов валют. Если рассматривать схему словаря как XML-файл, быстрый SAX-парсер - это все, что требуется, чтобы вытащить элементы <xsd:enumeration>, содержащие допустимые величины. При втором подходе коды валют хранятся в центральной реляционной базе данных. Тогда приложения могут обращаться к ее таблице напрямую, а схема словаря может быть динамически генерироваться из этой же таблицы. Любой из описанных методов обеспечивает синхронность наборов допустимых величин по приложениям.
Наконец, в-третьих, использование подобных схем словарей допустимо только в том случае, если изменения, вносимые в них, сводятся либо к добавлению перечисляемой величины, либо ее удалению.
Структура схем словарей не должна изменяться ни при каких условиях. Если в схему словаря было бы добавлено новое определение простого или сложного типа или элемента, это могло изменить результаты проверки допустимости экземпляра по основной схеме и привести к сбою в работе базового приложения. Таким образом, необходимо "проверять на допустимость" схемы словарей, чтобы быть уверенным, что они содержат только одно определение простого типа с перечисляемыми величинами. Именно такую ситуацию описал Уилл Провост (Will Provost) в своей статье "Работа с метасхемой" ("Working with a Metaschema").
Очевидным решением было бы написание в качестве метасхемы схемы для схем словарей. На практике автор статьи не стал бы это делать. Известно, что существующая "Схема для схем" ("Schema for Schemas") не на все 100% верна при описании синтаксиса, и именно по этой причине редакторы схем используют ее в качестве справочного, а не нормативного руководства. По этой же причине, а также из-за того, что формат схемы словаря довольно простой, воспользуемся следующей схемой Schematron (Schematron schema):
<sch:schema
xmlns = "http://www.w3.org/2001/XMLSchema"
xmlns:sch = "http://www.ascc.net/xml/schematron"
xmlns:xsd = "http://www.w3.org/2001/XMLSchema"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation =
"http://www.ascc.net/xml/schematron schematron-1.5.xsd">
<sch:title>Controlled vocabulary validation (Проверка допустимости
управляемого словаря)</sch:title>
<!-- The input is assumed to be a valid W3C XML Schema. -->
<!-- (Предполагается, входные данные допустимая W3C XML-схема). -->
<!-- This just checks that it is also a valid -->
<!-- vocabulary Schema. -->
<!-- (Ниже просто проверяется допустимость Схемы словаря). -->
<sch:pattern name = "controlled-vocabulary-schema">
<sch:rule context = "schema">
<sch:assert test = "count(*) = count(simpleType[@name])"
>The schema must contain only a
single simple type definition
(Эта схема должна содержать только
одно определение простого типа).</sch:assert>
<sch:assert test = "count(simpleType[@name]) = 1"
>The schema must contain a single simpleType
definition or a single include
(Эта схема должна содержать одно определение
simpleType или один элемент include).</sch:assert>
</sch:rule>
<sch:rule context = "simpleType">
<sch:assert test = "@name"
>The simpleType must have a name
(simpleType должен иметь атрибут name)..</sch:assert>
<sch:assert test = "count(restriction) = 1"
>The simpleType must contain a
single restriction
(simpleType должен содержать одно ограничение).</sch:assert>
<sch:assert test = "count(*) = count(annotation)+count(restriction)"
>The simpleType may have an annotation as well as its
restriction, but no other structure
(У simpleType может быть аннотация, а также свои
ограничения, но никакая другая структура).</sch:assert>
</sch:rule>
<sch:rule context = "restriction">
<sch:assert test = "enumeration"
>A restriction must contain enumerated values
(Ограничение должно содержать перечисляемые величины).</sch:assert>
</sch:rule>
<sch:rule context = "enumeration">
<sch:key name = "enumerationsByValue" path = "@value"/>
<sch:assert test = "count(key('enumerationsByValue', @value)) = 1"
>An enumerated value must be unique
(Перечисляемая величина должны быть уникальной).</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
Чтобы в среде Windows проверить на допустимость схему словаря по этой схеме Schematron, вы можете воспользоваться бесплатным валидатором от Topologi (free validator from Topologi). Касательно других платформ, смотрите список инструментальных средств в Справочнике ресурсов Schematron (Schematron Resource Directory). Более подробную информацию о Schematron можно найти в статье Чимези Огбуджи (Chimezie Ogbuji) "Проверка допустимости с помощью Schematron" ("Validating XML with Schematron").
Утверждения в Schematron выражаются с помощью выражений, значением которых должна быть true. Если их значением оказывается false, генерируется ошибка проверки допустимости по Schematron. При рассмотрении нашей схемы Schematron стоит отметить следующее:
- Обратите внимание на правило для контекста schema. Оно содержит утверждения, которые применяются к элементу <xsd:schema> в схеме словаря. Первое утверждение контролирует, что единственное, что содержится в схеме, это определения <xsd:simpleType>. Второе утверждение контролирует, что присутствует только одно определение <xsd:simpleType>.
- Правило для контекста simpleType утверждает, что, во-первых, у <xsd:simpleType> должен быть атрибут name, а, во-вторых, <xsd:simpleType> должен содержать <xsd:restriction> и может включать <xsd:annotation>, но больше никаких других элементов.
- Правило для контекста restriction утверждает, что <xsd:restriction>> должно содержать одну или более перечисляемых величин.
- Правило для контекста enumeration утверждает, что перечисляемые величины должны быть уникальны. Чтобы проверить это, используется ключ Schematron (эквивалентный ключу XSLT). Выражение key('enumerationsByValue', @value) возвращает список элементов <xsd:enumeration> с таким же значением, как и проверяемый на допустимость элемент. Если эти величины уникальны, то в этом списке будет присутствовать только один элемент <xsd:enumeration>, тот, который проверяется на допустимость.
Заключение
Поместив изменчивые управляемые словари (перечисления) в свои собственные схемы словарей, можно повысить управляемость W3C XML-схем. В этой статье было рассмотрено, как идентифицировать изменчивые управляемые словари, как отделять их от основной схемы, как развязывать версии и как проверять на допустимость схемы словарей. Разумеется, абсолютного рецепта - когда у управляемого словаря должна быть своя схема - нет. Поэтому применяя приведенные в этой статье рекомендации, всегда полагайтесь на здравый смысл и знание проблемной области.
Ресурсы
Файл с примерами, приведенными в этой статье можно скачать в виде ZIP-архива (9 кБ).