diff --git a/clearing-parent/backend-api/pom.xml b/clearing-parent/backend-api/pom.xml
index 5e9d02c10..bab1e7850 100644
--- a/clearing-parent/backend-api/pom.xml
+++ b/clearing-parent/backend-api/pom.xml
@@ -112,6 +112,11 @@
org.mockito
mockito-core
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+ test
+
diff --git a/clearing-parent/backend-api/src/main/resources/meta/dictionaries.xml b/clearing-parent/backend-api/src/main/resources/meta/dictionaries.xml
index 5f04c6b6d..d3148ff2e 100644
--- a/clearing-parent/backend-api/src/main/resources/meta/dictionaries.xml
+++ b/clearing-parent/backend-api/src/main/resources/meta/dictionaries.xml
@@ -62,7 +62,7 @@
-
+
@@ -76,7 +76,7 @@
-
+
diff --git a/clearing-parent/backend-api/src/main/resources/meta/meta.xml b/clearing-parent/backend-api/src/main/resources/meta/meta.xml
index 25d069830..f9c3ba5c8 100644
--- a/clearing-parent/backend-api/src/main/resources/meta/meta.xml
+++ b/clearing-parent/backend-api/src/main/resources/meta/meta.xml
@@ -1,6 +1,6 @@
-
+
@@ -920,7 +920,7 @@
-
+
@@ -1498,7 +1498,7 @@
-
+
@@ -1537,7 +1537,7 @@
-
+
@@ -1565,7 +1565,7 @@
-
+
@@ -1658,8 +1658,8 @@
-
-
+
+
@@ -1695,8 +1695,8 @@
-
-
+
+
@@ -1765,7 +1765,7 @@
-
+
diff --git a/clearing-parent/backend-api/src/test/java/ru/spcex/clearing/backendapi/meta/DataXmlTest.java b/clearing-parent/backend-api/src/test/java/ru/spcex/clearing/backendapi/meta/DataXmlTest.java
new file mode 100644
index 000000000..bb299c3de
--- /dev/null
+++ b/clearing-parent/backend-api/src/test/java/ru/spcex/clearing/backendapi/meta/DataXmlTest.java
@@ -0,0 +1,175 @@
+package ru.spcex.clearing.backendapi.meta;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Проверяет словари на опечатки: пустые значения, сочетание кириллицы и латиницы в одном слове.
+ * dictionaries.xml
+ * data_initial.xml
+ * data_register_code.xml
+ */
+public class DataXmlTest {
+ private Logger log = LoggerFactory.getLogger(getClass());
+
+ @Test
+ public void testMetaXml() throws IOException {
+ verifyXML(new File("src/main/resources/meta/meta.xml"));
+ }
+
+ /**
+ * Проверка XML (data.xml aka dictionaries.xml)
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testDictionariesXml() throws IOException {
+ verifyXML(new File("src/main/resources/meta/dictionaries.xml"));
+ }
+
+ @Test
+ public void testDataInitialXml() throws IOException {
+ verifyXML(new File("src/main/resources/meta/data_initial.xml"));
+ }
+
+ @Test
+ public void testDataRegisterCodeXml() throws IOException {
+ verifyXML(new File("src/main/resources/meta/data_register_code.xml"));
+ }
+
+ protected void verifyXML(File xmlFile) throws IOException {
+ XmlMapper xmlObjectMapper = new XmlMapper();
+ xmlObjectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
+ xmlObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ // Это юнит-тест с внутренними данными, парсинг DTD выключать не обязательно.
+ Map root = xmlObjectMapper.readValue(xmlFile, Map.class);
+ Assertions.assertNotNull(root, "Empty XML in file " + xmlFile);
+
+ String errors = verifyNode(xmlFile.getName(), root);
+ Assertions.assertNull(errors, errors);
+ }
+
+ private String verifyNode(String path, Map node) {
+ for (Map.Entry item : node.entrySet()) {
+ String name = item.getKey();
+ if (!verifyText(name)) {
+ return path + "> node name \"" + name + "\" has error";
+ }
+ String thisPath = path + "/" + name;
+ if (item.getValue() instanceof Map itemValue) {
+ String error = verifyNode(thisPath, itemValue);
+ if (error != null)
+ return error;
+ } else if (item.getValue() instanceof String text) {
+ if (!verifyText(text)) {
+ return thisPath + "> has error in content: \"" + text + "\"";
+ }
+ } else if (item.getValue() instanceof List values) {
+ for (Object o : values) {
+ if (o instanceof Map itemMap) {
+ String error = verifyNode(thisPath, itemMap);
+ if (error != null)
+ return error;
+ } else if (o instanceof String oVal) {
+ if (!verifyText(oVal)) {
+ return thisPath + "> one of values in node \"" + oVal + "\" has error";
+ }
+ } else if (o == null) {
+ // nothing
+ } else {
+ throw new IllegalArgumentException(thisPath + "> Unsupported one of value type " + o.getClass().getSimpleName() + " for verification");
+ }
+ }
+ } else if (item.getValue() == null) {
+ // nothing
+ } else {
+ throw new IllegalArgumentException(thisPath + "> Unsupported value type " + item.getValue().getClass().getSimpleName() + " for verification");
+ }
+ }
+ return null;
+ }
+
+ final char[] CYRILLICS;
+ final char[] LATINS;
+
+ {
+ CYRILLICS = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя".toCharArray();
+ LATINS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();
+ Arrays.sort(CYRILLICS);
+ Arrays.sort(LATINS);
+ }
+
+ /**
+ * Проверяет смесь кириллицы и латиницы в одном слове
+ *
+ * @param text
+ * @return
+ */
+ boolean verifyText(String text) {
+ boolean word = false, cyrillic = false, latin = false;
+ if (text != null) {
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ boolean isWhitespace = Character.isWhitespace(c);
+ boolean isCyrillic = Arrays.binarySearch(CYRILLICS, c) >= 0;
+ boolean isLatin = Arrays.binarySearch(LATINS, c) >= 0;
+ assert !(isCyrillic && isLatin);
+ assert !(isWhitespace && (isCyrillic || isLatin));
+ if (word && isWhitespace) {
+ word = false; // слово закончилось
+ if (cyrillic && latin)
+ return false; // Смесь кириллицы и латиницы
+ } else if (!word && !isWhitespace) {
+ word = true; // новое слово
+ cyrillic = latin = false;
+ }
+ cyrillic |= isCyrillic;
+ latin |= isLatin;
+ }
+ if (cyrillic && latin)
+ return false; // Смесь кириллицы и латиницы
+ }
+ return true;
+ }
+
+ @Test
+ public void selfTest() {
+ verifyText(null);
+ Assertions.assertTrue(verifyText(""));
+ Assertions.assertTrue(verifyText("A"));
+ Assertions.assertTrue(verifyText(" A "));
+ Assertions.assertTrue(verifyText("A call of"));
+ Assertions.assertTrue(verifyText("The spring of spin winter."));
+ Assertions.assertTrue(verifyText("Пользователь user текст обычный."));
+ Assertions.assertTrue(verifyText("АККаунт mixED 102E333C"));
+ Assertions.assertTrue(verifyText("АКК_аунт mi_xED 10-+*/2E333C"));
+ Assertions.assertTrue(verifyText("XML"));
+ Assertions.assertTrue(verifyText("Same sample apple"));
+ Assertions.assertTrue(verifyText("По tExT кРАям"));
+
+ Assertions.assertFalse(verifyText("Пользоватьель userтекст обычный."));
+ Assertions.assertFalse(verifyText("ХML"));
+ Assertions.assertFalse(verifyText("XМL"));
+ Assertions.assertFalse(verifyText("XMЛ"));
+ Assertions.assertFalse(verifyText(" ХML"));
+ Assertions.assertFalse(verifyText(" XМL"));
+ Assertions.assertFalse(verifyText(" XMЛ"));
+ Assertions.assertFalse(verifyText("ХML "));
+ Assertions.assertFalse(verifyText("XМL "));
+ Assertions.assertFalse(verifyText("XMЛ "));
+ Assertions.assertFalse(verifyText("12XM_Л34"));
+ Assertions.assertFalse(verifyText("Same sample аррlе"));
+ Assertions.assertFalse(verifyText("Same samplе apple"));
+ Assertions.assertFalse(verifyText("Same sample аpple"));
+ Assertions.assertFalse(verifyText("client_сode"));
+ }
+}
diff --git a/clearing-parent/db-scripts/src/main/resources/db/DICTIONARIES.sql b/clearing-parent/db-scripts/src/main/resources/db/DICTIONARIES.sql
index 5a4a31026..f27a74ca1 100644
--- a/clearing-parent/db-scripts/src/main/resources/db/DICTIONARIES.sql
+++ b/clearing-parent/db-scripts/src/main/resources/db/DICTIONARIES.sql
@@ -126,7 +126,7 @@ INSERT INTO DOCUMENT_TYPE_DICTIONARY(ID, CODE, NAME) values (20, 'LICD', 'Док
INSERT INTO DOCUMENT_TYPE_DICTIONARY(ID, CODE, NAME) values (21, 'LICT', 'Документ о лицензии по управлению ЦБ') ON CONFLICT (ID) DO UPDATE SET CODE = EXCLUDED.CODE, NAME = EXCLUDED.NAME;
-INSERT INTO DOCUMENT_TYPE_DICTIONARY(ID, CODE, NAME) values (22, 'LICС', 'Документ о лицензии по брокерской деятельности ПФИ (производных финансовых инструментов)') ON CONFLICT (ID) DO UPDATE SET CODE = EXCLUDED.CODE, NAME = EXCLUDED.NAME;
+INSERT INTO DOCUMENT_TYPE_DICTIONARY(ID, CODE, NAME) values (22, 'LICC', 'Документ о лицензии по брокерской деятельности ПФИ (производных финансовых инструментов)') ON CONFLICT (ID) DO UPDATE SET CODE = EXCLUDED.CODE, NAME = EXCLUDED.NAME;
INSERT INTO DOCUMENT_TYPE_DICTIONARY(ID, CODE, NAME) values (23, 'LICF', 'Документ о лицензии форекс-дилера') ON CONFLICT (ID) DO UPDATE SET CODE = EXCLUDED.CODE, NAME = EXCLUDED.NAME;
@@ -154,7 +154,7 @@ INSERT INTO COMPANY_SYMBOL_DICTIONARY(ID, CODE, NAME, SHORTNAME) values (10, 'LI
INSERT INTO COMPANY_SYMBOL_DICTIONARY(ID, CODE, NAME, SHORTNAME) values (11, 'LICT', 'Номер лицензии по управлению ЦБ', 'Управление ценными бумагами') ON CONFLICT (ID) DO UPDATE SET CODE = EXCLUDED.CODE, NAME = EXCLUDED.NAME, SHORTNAME = EXCLUDED.SHORTNAME;
-INSERT INTO COMPANY_SYMBOL_DICTIONARY(ID, CODE, NAME, SHORTNAME) values (12, 'LICС', 'Номер лицензии по брокерской деятельности ПФИ (производных финансовых инструментов)', 'Брокерская деятельность ПФИ') ON CONFLICT (ID) DO UPDATE SET CODE = EXCLUDED.CODE, NAME = EXCLUDED.NAME, SHORTNAME = EXCLUDED.SHORTNAME;
+INSERT INTO COMPANY_SYMBOL_DICTIONARY(ID, CODE, NAME, SHORTNAME) values (12, 'LICC', 'Номер лицензии по брокерской деятельности ПФИ (производных финансовых инструментов)', 'Брокерская деятельность ПФИ') ON CONFLICT (ID) DO UPDATE SET CODE = EXCLUDED.CODE, NAME = EXCLUDED.NAME, SHORTNAME = EXCLUDED.SHORTNAME;
INSERT INTO COMPANY_SYMBOL_DICTIONARY(ID, CODE, NAME, SHORTNAME) values (13, 'LICF', 'Номер лицензии форекс-дилера', 'Деятельность форекс-дилера') ON CONFLICT (ID) DO UPDATE SET CODE = EXCLUDED.CODE, NAME = EXCLUDED.NAME, SHORTNAME = EXCLUDED.SHORTNAME;