diff --git a/docs/api-write.md b/docs/api-write.md
new file mode 100644
index 000000000..4af3540ff
--- /dev/null
+++ b/docs/api-write.md
@@ -0,0 +1,70 @@
+# API записи метаданных и контракт для BSL Language Server
+
+В mdclasses реализован API записи объектов метаданных в файлы (EDT .mdo, при необходимости — Designer .xml). Для вызова из расширений IDE (например, vscode-1c-platform-tools) предполагается **protocol extension** в BSL Language Server: кастомные LSP-методы, которые вызывают mdclasses.
+
+Реализация самих LSP-методов выполняется в репозитории **bsl-language-server**; здесь описан рекомендуемый контракт для согласования с клиентом.
+
+## Рекомендуемые LSP-методы
+
+### mdclasses/readConfiguration
+
+Чтение конфигурации по пути к корню проекта (EDT или Designer).
+
+**Параметры (JSON-RPC):**
+
+- `path` (string) — путь к каталогу конфигурации (корень EDT-проекта или каталог с `Configuration.xml` для Designer).
+
+**Ответ:**
+
+- JSON-представление конфигурации или упрощённое дерево (имя, uuid, дочерние типы/объекты) для отображения в панели метаданных. Либо путь к считанной конфигурации и минимальный набор полей.
+
+**Вызов на стороне BSL LS:** `MDClasses.createConfiguration(Path.of(params.path))` или `MDOReader.readConfiguration(Path.of(params.path))`.
+
+---
+
+### mdclasses/writeObject
+
+Запись одного объекта метаданных в файл.
+
+**Параметры:**
+
+- `path` (string) — путь к файлу (`.mdo` для EDT или `.xml` для Designer).
+- `object` (string) — тип объекта, например `Subsystem`, `Configuration`, `Catalog`.
+- `data` (object) — данные объекта для сериализации (соответствуют полям модели mdclasses).
+
+**Ответ:**
+
+- `{ "success": true }` при успехе.
+- Ошибка (например, `UnsupportedOperationException`, `IOException`) в формате LSP.
+
+**Вызов на стороне BSL LS:** построить Java-объект (Subsystem, Configuration, Catalog и т.д.) из `data` и вызвать `MDClasses.writeObject(Path.of(path), object)` или `MDOWriter.writeObject(...)`.
+
+---
+
+### mdclasses/addObjectToConfiguration (опционально)
+
+Добавление нового объекта в дерево конфигурации: обновление `Configuration.mdo` (EDT) или корневого файла конфигурации (Designer).
+
+**Параметры:**
+
+- `configurationRoot` (string) — путь к корню конфигурации.
+- `objectType` (string) — тип объекта (например, `Subsystem`, `Catalog`).
+- `objectName` (string) — имя нового объекта.
+
+**Действие:** прочитать конфигурацию через MDOReader, добавить ссылку в соответствующий список (например, `Subsystem.Имя`), записать обновлённый файл конфигурации через API записи Configuration.
+
+Упрощает сценарий «добавить новую подсистему/справочник» без ручного разбора и записи Configuration.mdo на стороне клиента.
+
+---
+
+## Поддерживаемые типы и форматы (mdclasses)
+
+- **EDT (.mdo):** запись `Subsystem`, `Configuration`, `Catalog` (и при необходимости других типов по мере реализации конвертеров).
+- **Designer (.xml):** запись `Subsystem` (и при необходимости других типов).
+
+Формат определяется по расширению пути в `writeObject`.
+
+## См. также
+
+- План [API записи метаданных и интеграция с VSCode extension](https://github.com/1c-syntax/mdclasses/issues/158).
+- Реализация в mdclasses: пакеты `writer`, `writer.edt`, `writer.designer`.
diff --git a/scripts/run-read-write-demo.bat b/scripts/run-read-write-demo.bat
new file mode 100644
index 000000000..7dc9b0e77
--- /dev/null
+++ b/scripts/run-read-write-demo.bat
@@ -0,0 +1,5 @@
+@echo off
+REM Demo: read-write API for metadata (issue #158).
+REM Creates a Subsystem, writes to build/read-write-demo-output, reads back and prints OK.
+cd /d "%~dp0.."
+call gradlew.bat runReadWriteDemo %*
diff --git a/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDCWriteSettings.java b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDCWriteSettings.java
new file mode 100644
index 000000000..1a0584e72
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDCWriteSettings.java
@@ -0,0 +1,51 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.mdclasses;
+
+import lombok.Builder;
+import org.jspecify.annotations.Nullable;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Настройки записи объектов метаданных в файлы (EDT и Designer).
+ * Используется при вызове {@link com.github._1c_syntax.bsl.mdclasses.MDClasses#writeObject(java.nio.file.Path, Object, MDCWriteSettings)}.
+ *
+ * @param encoding Кодировка записываемых файлов (по умолчанию UTF-8)
+ */
+@Builder
+public record MDCWriteSettings(@Nullable String encoding) {
+
+ /**
+ * Настройки по умолчанию: кодировка UTF-8.
+ */
+ public static final MDCWriteSettings DEFAULT = MDCWriteSettings.builder()
+ .encoding(StandardCharsets.UTF_8.name())
+ .build();
+
+ /**
+ * Возвращает кодировку для записи файлов; при null в настройках возвращается UTF-8.
+ */
+ public String encoding() {
+ return encoding != null ? encoding : StandardCharsets.UTF_8.name();
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDClasses.java b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDClasses.java
index 5a729c25d..328a2e421 100644
--- a/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDClasses.java
+++ b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDClasses.java
@@ -24,9 +24,12 @@
import com.github._1c_syntax.bsl.reader.MDMerger;
import com.github._1c_syntax.bsl.reader.MDOReader;
import com.github._1c_syntax.bsl.types.MDOType;
+import com.github._1c_syntax.bsl.writer.MDOWriter;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
@@ -66,6 +69,29 @@ public ExternalSource createExternalReport() {
return ExternalReport.EMPTY;
}
+ /**
+ * Записывает объект метаданных в файл (формат по расширению пути: .mdo — EDT, .xml — Designer).
+ *
+ * @param path Путь к файлу (например, .../Subsystems/Name/Name.mdo или .../Subsystems/Name.xml)
+ * @param object Объект метаданных (поддерживается Subsystem, Catalog, Configuration)
+ * @throws IOException при ошибке записи
+ */
+ public void writeObject(@NonNull Path path, @NonNull Object object) throws IOException {
+ MDOWriter.writeObject(path, object);
+ }
+
+ /**
+ * Записывает объект метаданных в файл с настройками записи.
+ *
+ * @param path Путь к файлу
+ * @param object Объект метаданных
+ * @param writeSettings Настройки записи
+ * @throws IOException при ошибке записи
+ */
+ public void writeObject(@NonNull Path path, @NonNull Object object, @Nullable MDCWriteSettings writeSettings) throws IOException {
+ MDOWriter.writeObject(path, object, writeSettings);
+ }
+
/**
* Создает конфигурацию или расширение по указанному пути
*
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/MDOWriter.java b/src/main/java/com/github/_1c_syntax/bsl/writer/MDOWriter.java
new file mode 100644
index 000000000..85ffe9ae0
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/MDOWriter.java
@@ -0,0 +1,75 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer;
+
+import com.github._1c_syntax.bsl.mdclasses.MDCWriteSettings;
+import com.github._1c_syntax.bsl.writer.designer.DesignerWriter;
+import com.github._1c_syntax.bsl.writer.edt.EDTWriter;
+import lombok.experimental.UtilityClass;
+import org.apache.commons.io.FilenameUtils;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+/**
+ * Фасад записи объектов метаданных в файлы (EDT .mdo и Designer .xml).
+ */
+@UtilityClass
+public class MDOWriter {
+
+ /**
+ * Записывает объект метаданных в файл.
+ * Формат определяется по расширению пути: .mdo — EDT, .xml — Designer.
+ *
+ * @param path Путь к файлу (например, .../Subsystems/Name/Name.mdo или .../Subsystems/Name.xml)
+ * @param object Объект метаданных (Subsystem, Catalog, Configuration)
+ * @throws IOException при ошибке записи
+ * @throws UnsupportedOperationException если формат или тип объекта не поддерживается
+ */
+ public void writeObject(Path path, Object object) throws IOException {
+ writeObject(path, object, MDCWriteSettings.DEFAULT);
+ }
+
+ /**
+ * Записывает объект метаданных в файл с настройками.
+ *
+ * @param path Путь к файлу (расширение .mdo или .xml определяет формат)
+ * @param object Объект метаданных (Subsystem, Catalog, Configuration)
+ * @param writeSettings Настройки записи (кодировка и др.); может быть null — тогда используются настройки по умолчанию
+ * @throws IOException при ошибке записи
+ * @throws UnsupportedOperationException если формат или тип объекта не поддерживается
+ */
+ public void writeObject(Path path, Object object, MDCWriteSettings writeSettings) throws IOException {
+ if (path == null || object == null) {
+ throw new IllegalArgumentException("path and object must not be null");
+ }
+ if (FilenameUtils.isExtension(path.toString(), "mdo")) {
+ var writer = new EDTWriter(writeSettings);
+ writer.write(path, object);
+ } else if (FilenameUtils.isExtension(path.toString(), "xml")) {
+ var writer = new DesignerWriter(writeSettings);
+ writer.write(path, object);
+ } else {
+ throw new UnsupportedOperationException("Write is supported only for EDT (.mdo) or Designer (.xml) format, got: " + path);
+ }
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/ReadWriteDemo.java b/src/main/java/com/github/_1c_syntax/bsl/writer/ReadWriteDemo.java
new file mode 100644
index 000000000..39df03536
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/ReadWriteDemo.java
@@ -0,0 +1,135 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer;
+
+import com.github._1c_syntax.bsl.mdclasses.Configuration;
+import com.github._1c_syntax.bsl.mdclasses.MDClasses;
+import com.github._1c_syntax.bsl.mdclasses.MDCReadSettings;
+import com.github._1c_syntax.bsl.mdo.Catalog;
+import com.github._1c_syntax.bsl.mdo.Subsystem;
+import com.github._1c_syntax.bsl.reader.MDOReader;
+import com.github._1c_syntax.bsl.reader.edt.EDTReader;
+import com.github._1c_syntax.bsl.types.MultiLanguageString;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * Демонстрация чтения и записи метаданных: три типа объектов (Subsystem, Catalog, Configuration)
+ * в двух форматах — EDT (.mdo) и Конфигуратор (Designer .xml). Артефакты раскладываются по каталогам:
+ * build/read-write-demo-output/edt/ и build/read-write-demo-output/designer/.
+ * Запуск: {@code ./gradlew runReadWriteDemo} или указать путь в аргументе.
+ */
+public final class ReadWriteDemo {
+
+ private ReadWriteDemo() {
+ }
+
+ /**
+ * Точка входа: создаёт примеры Subsystem, Catalog, Configuration и записывает их в EDT и Designer.
+ *
+ * @param args необязательный путь к каталогу вывода; по умолчанию build/read-write-demo-output
+ */
+ public static void main(String[] args) throws Exception {
+ var baseDir = args.length > 0 ? Paths.get(args[0]) : Paths.get("build", "read-write-demo-output");
+ var edtDir = baseDir.resolve("edt");
+ var designerDir = baseDir.resolve("designer");
+ var edtSrc = edtDir.resolve("src");
+ var designerSrc = designerDir.resolve("src").resolve("cf");
+ Files.createDirectories(edtSrc);
+ Files.createDirectories(designerSrc);
+
+ var subsystem = Subsystem.builder()
+ .name("DemoSubsystem")
+ .uuid("0421b67e-ed26-491d-ab98-ec59002ed4ce")
+ .synonym(MultiLanguageString.create("ru", "Демо подсистема"))
+ .build();
+ var catalog = Catalog.builder()
+ .name("DemoCatalog")
+ .uuid("c6c26c3c-de7a-4ed4-944d-ada62cf1ab8f")
+ .synonym(MultiLanguageString.create("ru", "Демо справочник"))
+ .build();
+ var config = Configuration.builder()
+ .name("DemoConfiguration")
+ .uuid("46c7c1d0-b04d-4295-9b04-ae3207c18d29")
+ .build();
+
+ var ok = true;
+
+ // --- EDT (.mdo) — каталог edt/ ---
+ System.out.println("=== EDT (edt/) ===");
+ var subsystemMdo = edtSrc.resolve("Subsystems").resolve("DemoSubsystem").resolve("DemoSubsystem.mdo");
+ MDClasses.writeObject(subsystemMdo, subsystem);
+ System.out.println("Written: " + subsystemMdo.toAbsolutePath());
+ var readSub = new EDTReader(subsystemMdo, MDCReadSettings.SKIP_SUPPORT).read(subsystemMdo);
+ ok &= checkReadBack("Subsystem", readSub instanceof Subsystem s ? s.getName() : null, subsystem.getName());
+
+ var catalogMdo = edtSrc.resolve("Catalogs").resolve("DemoCatalog").resolve("DemoCatalog.mdo");
+ MDClasses.writeObject(catalogMdo, catalog);
+ System.out.println("Written: " + catalogMdo.toAbsolutePath());
+ var readCat = new EDTReader(catalogMdo, MDCReadSettings.SKIP_SUPPORT).read(catalogMdo);
+ ok &= checkReadBack("Catalog", readCat instanceof Catalog c ? c.getName() : null, catalog.getName());
+
+ var configMdo = edtSrc.resolve("Configuration").resolve("Configuration.mdo");
+ MDClasses.writeObject(configMdo, config);
+ System.out.println("Written: " + configMdo.toAbsolutePath());
+ var readConfig = MDOReader.readConfiguration(configMdo, MDCReadSettings.SKIP_SUPPORT);
+ if (readConfig != null && readConfig instanceof Configuration) {
+ System.out.println(" Read back Configuration: " + readConfig.getClass().getSimpleName());
+ } else {
+ System.out.println(" ERROR: read back Configuration failed");
+ ok = false;
+ }
+
+ // --- Конфигуратор (Designer .xml) — каталог designer/ ---
+ System.out.println("=== Designer (designer/) ===");
+ var subsystemXml = designerSrc.resolve("Subsystems").resolve("DemoSubsystem.xml");
+ MDClasses.writeObject(subsystemXml, subsystem);
+ System.out.println("Written: " + subsystemXml.toAbsolutePath());
+ ok &= Files.exists(subsystemXml) && Files.size(subsystemXml) > 0;
+
+ var catalogXml = designerSrc.resolve("Catalogs").resolve("DemoCatalog.xml");
+ MDClasses.writeObject(catalogXml, catalog);
+ System.out.println("Written: " + catalogXml.toAbsolutePath());
+ ok &= Files.exists(catalogXml) && Files.size(catalogXml) > 0;
+
+ var configXml = designerSrc.resolve("Configuration.xml");
+ MDClasses.writeObject(configXml, config);
+ System.out.println("Written: " + configXml.toAbsolutePath());
+ ok &= Files.exists(configXml) && Files.size(configXml) > 0;
+
+ if (ok) {
+ System.out.println("OK: all objects written to edt/ and designer/ (Subsystem, Catalog, Configuration).");
+ } else {
+ System.exit(1);
+ }
+ }
+
+ private static boolean checkReadBack(String type, String readName, String expectedName) {
+ if (readName != null && readName.equals(expectedName)) {
+ System.out.println(" Read back " + type + ": " + readName);
+ return true;
+ }
+ System.out.println(" ERROR: read back " + type + " failed (expected " + expectedName + ", got " + readName + ")");
+ return false;
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/designer/CatalogDesignerWriteConverter.java b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/CatalogDesignerWriteConverter.java
new file mode 100644
index 000000000..e2445a3db
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/CatalogDesignerWriteConverter.java
@@ -0,0 +1,129 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer.designer;
+
+import com.github._1c_syntax.bsl.mdo.Catalog;
+import com.github._1c_syntax.bsl.types.MdoReference;
+import com.github._1c_syntax.bsl.types.MultiLanguageString;
+import com.thoughtworks.xstream.converters.Converter;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+/**
+ * Конвертер записи справочника в формате Конфигуратора (Designer .xml).
+ * MVP: Name, Synonym, Comment, базовые свойства (UseStandardCommands, LevelCount, CodeLength и т.д.).
+ * Часть свойств (Hierarchical, HierarchyType, LevelCount, CodeLength и т.д.) задаётся значениями по умолчанию,
+ * так как в модели {@link com.github._1c_syntax.bsl.mdo.Catalog} пока нет соответствующих полей; при их появлении нужно перейти на чтение из catalog.
+ */
+public class CatalogDesignerWriteConverter implements Converter {
+
+ private static final String PROPERTIES = "Properties";
+ private static final String NAME = "Name";
+ private static final String SYNONYM = "Synonym";
+ private static final String COMMENT = "Comment";
+ private static final String V8_ITEM = "v8:item";
+ private static final String V8_LANG = "v8:lang";
+ private static final String V8_CONTENT = "v8:content";
+ private static final String FALSE = "false";
+
+ /** Сериализует справочник в Designer XML (Properties: Name, Synonym, Comment и др.). */
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ var catalog = (Catalog) source;
+
+ if (catalog.getUuid() != null && !catalog.getUuid().isEmpty()) {
+ writer.addAttribute("uuid", catalog.getUuid());
+ }
+ writer.startNode(PROPERTIES);
+ writeElement(writer, NAME, catalog.getName());
+ writeSynonym(writer, catalog.getSynonym());
+ writeElement(writer, COMMENT, catalog.getComment() != null ? catalog.getComment() : "");
+ // Catalog model only has getOwners(), getCodeSeries(), isCheckUnique() for these; rest are defaults until model is extended
+ writeElement(writer, "Hierarchical", "true");
+ writeElement(writer, "HierarchyType", "HierarchyFoldersAndItems");
+ writeElement(writer, "LimitLevelCount", FALSE);
+ writeElement(writer, "LevelCount", "2");
+ writeElement(writer, "FoldersOnTop", "true");
+ writeElement(writer, "UseStandardCommands", "true");
+ writeElement(writer, "Owners", formatOwners(catalog.getOwners()));
+ writeElement(writer, "SubordinationUse", "ToItems");
+ writeElement(writer, "CodeLength", "9");
+ writeElement(writer, "DescriptionLength", "25");
+ writeElement(writer, "CodeType", "String");
+ writeElement(writer, "CodeAllowedLength", "Variable");
+ writeElement(writer, "CodeSeries", catalog.getCodeSeries() != null ? catalog.getCodeSeries().fullName().getEn() : "WholeCatalog");
+ writeElement(writer, "CheckUnique", catalog.isCheckUnique() ? "true" : FALSE);
+ writeElement(writer, "Autonumbering", FALSE);
+ writeElement(writer, "DefaultPresentation", "AsDescription");
+ writer.endNode(); // Properties
+ }
+
+ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageString synonym) {
+ if (synonym == null || synonym.isEmpty()) {
+ return;
+ }
+ writer.startNode(SYNONYM);
+ for (var entry : synonym.getContent()) {
+ writer.startNode(V8_ITEM);
+ writeElement(writer, V8_LANG, entry.getLangKey());
+ writeElement(writer, V8_CONTENT, entry.getValue());
+ writer.endNode();
+ }
+ writer.endNode();
+ }
+
+ private static String formatOwners(List owners) {
+ if (owners == null || owners.isEmpty()) {
+ return "";
+ }
+ return owners.stream()
+ .filter(ref -> ref != null && ref.getMdoRef() != null && !ref.getMdoRef().isEmpty())
+ .map(MdoReference::getMdoRef)
+ .collect(Collectors.joining(", "));
+ }
+
+ private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) {
+ if (text == null) {
+ return;
+ }
+ writer.startNode(nodeName);
+ writer.setValue(text);
+ writer.endNode();
+ }
+
+ /** Конвертер только для записи; чтение не поддерживается. */
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ throw new UnsupportedOperationException("CatalogDesignerWriteConverter is for writing only");
+ }
+
+ /** Поддерживается только тип {@link com.github._1c_syntax.bsl.mdo.Catalog}. */
+ @Override
+ public boolean canConvert(Class type) {
+ return Catalog.class.isAssignableFrom(type);
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/designer/ConfigurationDesignerWriteConverter.java b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/ConfigurationDesignerWriteConverter.java
new file mode 100644
index 000000000..931b1c34b
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/ConfigurationDesignerWriteConverter.java
@@ -0,0 +1,166 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer.designer;
+
+import com.github._1c_syntax.bsl.mdo.MD;
+import com.github._1c_syntax.bsl.mdclasses.Configuration;
+import com.github._1c_syntax.bsl.types.MDOType;
+import com.github._1c_syntax.bsl.types.MultiLanguageString;
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+import java.util.List;
+
+/**
+ * Конвертер записи конфигурации в формате Конфигуратора (Designer Configuration.xml).
+ * MVP: Name, Synonym, Comment, uuid, ChildObjects (списки подсистем, справочников и т.д. по имени).
+ */
+public class ConfigurationDesignerWriteConverter implements Converter {
+
+ private static final String PROPERTIES = "Properties";
+ private static final String NAME = "Name";
+ private static final String SYNONYM = "Synonym";
+ private static final String COMMENT = "Comment";
+ private static final String CHILD_OBJECTS = "ChildObjects";
+ private static final String V8_ITEM = "v8:item";
+ private static final String V8_LANG = "v8:lang";
+ private static final String V8_CONTENT = "v8:content";
+
+ /** Сериализует конфигурацию в Designer XML (Properties, ChildObjects). */
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ var config = (Configuration) source;
+
+ if (config.getUuid() != null && !config.getUuid().isEmpty()) {
+ writer.addAttribute("uuid", config.getUuid());
+ }
+ writer.startNode(PROPERTIES);
+ writeElement(writer, NAME, config.getName());
+ writeSynonym(writer, config.getSynonym());
+ writeElement(writer, COMMENT, config.getComment() != null ? config.getComment() : "");
+ writer.endNode(); // Properties
+
+ writer.startNode(CHILD_OBJECTS);
+ writeChildList(writer, config.getSubsystems());
+ writeChildList(writer, config.getCatalogs());
+ writeChildList(writer, config.getDocuments());
+ writeChildList(writer, config.getEnums());
+ writeChildList(writer, config.getReports());
+ writeChildList(writer, config.getDataProcessors());
+ writeChildList(writer, config.getCommonModules());
+ writeChildList(writer, config.getRoles());
+ writeChildList(writer, config.getInterfaces());
+ writeChildList(writer, config.getConstants());
+ writeChildList(writer, config.getCommonForms());
+ writeChildList(writer, config.getCommonCommands());
+ writeChildList(writer, config.getFilterCriteria());
+ writeChildList(writer, config.getExchangePlans());
+ writeChildList(writer, config.getSessionParameters());
+ writeChildList(writer, config.getSettingsStorages());
+ writeChildList(writer, config.getFunctionalOptions());
+ writeChildList(writer, config.getBots());
+ writeChildList(writer, config.getFunctionalOptionsParameters());
+ writeChildList(writer, config.getDefinedTypes());
+ writeChildList(writer, config.getCommonTemplates());
+ writeChildList(writer, config.getCommonPictures());
+ writeChildList(writer, config.getCommonAttributes());
+ writeChildList(writer, config.getXDTOPackages());
+ writeChildList(writer, config.getWebServices());
+ writeChildList(writer, config.getWebSocketClients());
+ writeChildList(writer, config.getHttpServices());
+ writeChildList(writer, config.getWsReferences());
+ writeChildList(writer, config.getIntegrationServices());
+ writeChildList(writer, config.getEventSubscriptions());
+ writeChildList(writer, config.getScheduledJobs());
+ writeChildList(writer, config.getDocumentNumerators());
+ writeChildList(writer, config.getSequences());
+ writeChildList(writer, config.getDocumentJournals());
+ writeChildList(writer, config.getInformationRegisters());
+ writeChildList(writer, config.getAccumulationRegisters());
+ writeChildList(writer, config.getChartsOfCharacteristicTypes());
+ writeChildList(writer, config.getChartsOfAccounts());
+ writeChildList(writer, config.getAccountingRegisters());
+ writeChildList(writer, config.getChartsOfCalculationTypes());
+ writeChildList(writer, config.getCalculationRegisters());
+ writeChildList(writer, config.getBusinessProcesses());
+ writeChildList(writer, config.getTasks());
+ writeChildList(writer, config.getExternalDataSources());
+ writeChildList(writer, config.getStyles());
+ writeChildList(writer, config.getStyleItems());
+ writeChildList(writer, config.getLanguages());
+ writeChildList(writer, config.getCommandGroups());
+ writeChildList(writer, config.getPaletteColors());
+ writer.endNode(); // ChildObjects
+ }
+
+ private static void writeChildList(HierarchicalStreamWriter writer, List extends MD> list) {
+ if (list == null) {
+ return;
+ }
+ for (var obj : list) {
+ if (obj != null && obj.getName() != null) {
+ var type = obj.getMdoType();
+ if (type != null && type != MDOType.UNKNOWN) {
+ writeElement(writer, type.nameEn(), obj.getName());
+ }
+ }
+ }
+ }
+
+ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageString synonym) {
+ if (synonym == null || synonym.isEmpty()) {
+ return;
+ }
+ writer.startNode(SYNONYM);
+ for (var entry : synonym.getContent()) {
+ writer.startNode(V8_ITEM);
+ writeElement(writer, V8_LANG, entry.getLangKey());
+ writeElement(writer, V8_CONTENT, entry.getValue());
+ writer.endNode();
+ }
+ writer.endNode();
+ }
+
+ private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) {
+ if (text == null) {
+ return;
+ }
+ writer.startNode(nodeName);
+ writer.setValue(text);
+ writer.endNode();
+ }
+
+ /** Конвертер только для записи; чтение не поддерживается. */
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ throw new UnsupportedOperationException("ConfigurationDesignerWriteConverter is for writing only");
+ }
+
+ /** Поддерживается только тип {@link Configuration}. */
+ @Override
+ public boolean canConvert(Class type) {
+ return Configuration.class.isAssignableFrom(type);
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/designer/DesignerWriter.java b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/DesignerWriter.java
new file mode 100644
index 000000000..6f8610c24
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/DesignerWriter.java
@@ -0,0 +1,107 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer.designer;
+
+import com.github._1c_syntax.bsl.mdclasses.Configuration;
+import com.github._1c_syntax.bsl.mdclasses.MDCWriteSettings;
+import com.github._1c_syntax.bsl.mdo.Catalog;
+import com.github._1c_syntax.bsl.mdo.Subsystem;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
+import com.thoughtworks.xstream.io.xml.StaxDriver;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Запись объектов метаданных в формате Конфигуратора (Designer .xml).
+ * Поддерживаются только типы: Subsystem, Catalog, Configuration.
+ * ConfigurationExtension и другие реализации CF в данной версии не поддерживаются.
+ */
+@Slf4j
+public class DesignerWriter {
+
+ private static final String MD_NS = "http://v8.1c.ru/8.3/MDClasses";
+ private static final String V8_NS = "http://v8.1c.ru/8.1/data/core";
+
+ private final XStream xstream;
+ private final MDCWriteSettings writeSettings;
+
+ /**
+ * Создаёт писатель Designer XML с заданными настройками.
+ *
+ * @param writeSettings настройки записи (кодировка и др.); null заменяется на {@link MDCWriteSettings#DEFAULT}
+ */
+ public DesignerWriter(MDCWriteSettings writeSettings) {
+ this.writeSettings = writeSettings != null ? writeSettings : MDCWriteSettings.DEFAULT;
+ this.xstream = createXStream();
+ }
+
+ /**
+ * Записывает объект в файл .xml (формат Конфигуратора).
+ *
+ * @param path Путь к файлу .xml (родительские каталоги создаются при необходимости)
+ * @param object Объект метаданных (поддерживаются только Subsystem, Catalog, Configuration)
+ * @throws IOException при ошибке записи
+ * @throws UnsupportedOperationException если тип object не Subsystem, Catalog или Configuration
+ */
+ public void write(Path path, Object object) throws IOException {
+ if (object == null) {
+ throw new IllegalArgumentException("object must not be null");
+ }
+ if (!(object instanceof Subsystem) && !(object instanceof Catalog) && !(object instanceof Configuration)) {
+ throw new UnsupportedOperationException(
+ "Designer write supports only Subsystem, Catalog, Configuration, got: " + object.getClass().getName());
+ }
+ Path parent = path.getParent();
+ if (parent != null && !Files.exists(parent)) {
+ Files.createDirectories(parent);
+ }
+ var charset = Charset.forName(writeSettings.encoding());
+ try (Writer writer = new OutputStreamWriter(Files.newOutputStream(path), charset)) {
+ writer.write("\n");
+ writer.write("\n");
+ var prettyWriter = new PrettyPrintWriter(writer);
+ xstream.marshal(object, prettyWriter);
+ writer.write("\n");
+ }
+ }
+
+ private XStream createXStream() {
+ var driver = new StaxDriver();
+ var x = new XStream(driver);
+ x.alias("Subsystem", Subsystem.class);
+ x.alias("Catalog", Catalog.class);
+ x.alias("Configuration", Configuration.class);
+ x.registerConverter(new SubsystemDesignerWriteConverter(), XStream.PRIORITY_VERY_HIGH);
+ x.registerConverter(new CatalogDesignerWriteConverter(), XStream.PRIORITY_VERY_HIGH);
+ x.registerConverter(new ConfigurationDesignerWriteConverter(), XStream.PRIORITY_VERY_HIGH);
+ return x;
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/designer/SubsystemDesignerWriteConverter.java b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/SubsystemDesignerWriteConverter.java
new file mode 100644
index 000000000..5d2205c44
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/SubsystemDesignerWriteConverter.java
@@ -0,0 +1,146 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer.designer;
+
+import com.github._1c_syntax.bsl.mdo.Subsystem;
+import com.github._1c_syntax.bsl.types.MultiLanguageString;
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Конвертер записи подсистемы в формате Конфигуратора (Designer .xml).
+ * Выводит обёртку MetaDataObject и элемент Subsystem с Properties (Name, Synonym, Explanation, дочерние подсистемы).
+ * Ограничение: элемент Content (состав подсистемы по ссылкам) не выводится — в Designer он в формате xr:Item.
+ */
+public class SubsystemDesignerWriteConverter implements Converter {
+
+ private static final String SUBSYSTEM = "Subsystem";
+ private static final String PROPERTIES = "Properties";
+ private static final String NAME = "Name";
+ private static final String SYNONYM = "Synonym";
+ private static final String V8_ITEM = "v8:item";
+ private static final String V8_LANG = "v8:lang";
+ private static final String V8_CONTENT = "v8:content";
+ private static final String FALSE = "false";
+ private static final Set ALLOW_EMPTY_NODES = Set.of("Comment", "Explanation", "Picture", "Content");
+ /** Preferred locale order for Explanation when Designer format only allows a single string. */
+ private static final List EXPLANATION_LOCALE_ORDER = List.of("ru", "en");
+
+ /** Сериализует подсистему в Designer XML (Properties, ChildObjects). */
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ var subsystem = (Subsystem) source;
+
+ if (subsystem.getUuid() != null && !subsystem.getUuid().isEmpty()) {
+ writer.addAttribute("uuid", subsystem.getUuid());
+ }
+ writer.startNode(PROPERTIES);
+ writeElement(writer, NAME, subsystem.getName());
+ writeSynonym(writer, subsystem.getSynonym());
+ writeElement(writer, "Comment", subsystem.getComment() != null ? subsystem.getComment() : "");
+ writeElement(writer, "IncludeHelpInContents", subsystem.isIncludeHelpInContents() ? "true" : FALSE);
+ writeElement(writer, "IncludeInCommandInterface", subsystem.isIncludeInCommandInterface() ? "true" : FALSE);
+ writeElement(writer, "UseOneCommand", FALSE);
+ writeElement(writer, "Explanation", explanationToString(subsystem.getExplanation()));
+ writeElement(writer, "Picture", "");
+ // Content: Designer uses ; not written here (namespace xr not in root)
+ writeElement(writer, "Content", "");
+ writer.endNode(); // Properties
+
+ var children = subsystem.getSubsystems();
+ if (children != null && !children.isEmpty()) {
+ writer.startNode("ChildObjects");
+ for (var child : children) {
+ writeElement(writer, SUBSYSTEM, child.getName());
+ }
+ writer.endNode(); // ChildObjects
+ }
+ }
+
+ /**
+ * Преобразует мультиязычное пояснение в одну строку для элемента Designer Explanation.
+ * В формате Designer элемент Explanation — одно строковое значение, мультиязычность не поддерживается.
+ * Используется подход (A): выбор по предпочтительному порядку локалей — сначала "ru", затем "en",
+ * затем первая доступная запись. Остальные переводы не выводятся.
+ */
+ private static String explanationToString(MultiLanguageString explanation) {
+ if (explanation == null || explanation.isEmpty()) {
+ return "";
+ }
+ var content = explanation.getContent();
+ if (content == null || content.isEmpty()) {
+ return "";
+ }
+ for (var locale : EXPLANATION_LOCALE_ORDER) {
+ var value = content.stream()
+ .filter(e -> locale.equals(e.getLangKey()))
+ .findFirst()
+ .map(e -> e.getValue())
+ .orElse(null);
+ if (value != null && !value.isEmpty()) {
+ return value;
+ }
+ }
+ return content.iterator().next().getValue();
+ }
+
+ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageString synonym) {
+ if (synonym == null || synonym.isEmpty()) {
+ return;
+ }
+ writer.startNode(SYNONYM);
+ for (var entry : synonym.getContent()) {
+ writer.startNode(V8_ITEM);
+ writeElement(writer, V8_LANG, entry.getLangKey());
+ writeElement(writer, V8_CONTENT, entry.getValue());
+ writer.endNode();
+ }
+ writer.endNode();
+ }
+
+ private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) {
+ if (text == null && !ALLOW_EMPTY_NODES.contains(nodeName)) {
+ return;
+ }
+ writer.startNode(nodeName);
+ writer.setValue(text != null ? text : "");
+ writer.endNode();
+ }
+
+ /** Конвертер только для записи; чтение не поддерживается. */
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ throw new UnsupportedOperationException("SubsystemDesignerWriteConverter is for writing only");
+ }
+
+ /** Поддерживается только тип {@link Subsystem}. */
+ @Override
+ public boolean canConvert(Class type) {
+ return Subsystem.class.isAssignableFrom(type);
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/edt/CatalogEdtWriteConverter.java b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/CatalogEdtWriteConverter.java
new file mode 100644
index 000000000..405f73fb8
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/CatalogEdtWriteConverter.java
@@ -0,0 +1,121 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer.edt;
+
+import com.github._1c_syntax.bsl.mdo.Catalog;
+import com.github._1c_syntax.bsl.types.MultiLanguageString;
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+/**
+ * Конвертер записи справочника в формате EDT (.mdo).
+ * MVP: name, uuid, synonym, checkUnique, codeSeries.
+ * Часть свойств (useStandardCommands, levelCount, codeLength и т.д.) задаётся значениями по умолчанию,
+ * так как в модели {@link Catalog} пока нет соответствующих полей; при их появлении нужно перейти на чтение из catalog.
+ */
+public class CatalogEdtWriteConverter implements Converter {
+
+ private static final String MDCLASS_NS = "http://g5.1c.ru/v8/dt/metadata/mdclass";
+ private static final String NAME = "name";
+ private static final String KEY = "key";
+ private static final String VALUE = "value";
+ private static final String SYNONYM = "synonym";
+
+ /** Сериализует справочник в EDT XML (name, synonym, checkUnique, codeSeries и др.). */
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ var catalog = (Catalog) source;
+
+ if (catalog.getExplanation() != null && !catalog.getExplanation().isEmpty()) {
+ throw new IllegalStateException(
+ "EDT Catalog write does not support non-empty explanation, catalog: " + catalog.getName());
+ }
+ if (catalog.getOwners() != null && !catalog.getOwners().isEmpty()) {
+ throw new IllegalStateException(
+ "EDT Catalog write does not support non-empty owners, catalog: " + catalog.getName());
+ }
+
+ writer.addAttribute("xmlns:mdclass", MDCLASS_NS);
+ if (catalog.getUuid() != null && !catalog.getUuid().isEmpty()) {
+ writer.addAttribute("uuid", catalog.getUuid());
+ }
+
+ writeElement(writer, NAME, catalog.getName());
+ // Catalog model only has getCodeSeries(), isCheckUnique() for EDT catalog props; rest are defaults until model is extended
+ writeElement(writer, "useStandardCommands", "true");
+ writeElement(writer, "fullTextSearchOnInputByString", "DontUse");
+ writeElement(writer, "createOnInput", "Use");
+ writeElement(writer, "dataLockControlMode", "Managed");
+ writeElement(writer, "fullTextSearch", "Use");
+ writeElement(writer, "levelCount", "2");
+ writeElement(writer, "foldersOnTop", "true");
+ writeElement(writer, "codeLength", "9");
+ writeElement(writer, "descriptionLength", "25");
+ writeElement(writer, "codeType", "String");
+ writeElement(writer, "codeAllowedLength", "Variable");
+ writeElement(writer, "checkUnique", catalog.isCheckUnique() ? "true" : "false");
+ writeElement(writer, "autonumbering", "false");
+ writeElement(writer, "defaultPresentation", "AsDescription");
+ if (catalog.getCodeSeries() != null) {
+ writeElement(writer, "codeSeries", catalog.getCodeSeries().fullName().getEn());
+ }
+ writeElement(writer, "editType", "InDialog");
+ writeElement(writer, "choiceMode", "BothWays");
+ writeSynonym(writer, catalog.getSynonym());
+ }
+
+ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageString synonym) {
+ if (synonym == null || synonym.isEmpty()) {
+ return;
+ }
+ for (var entry : synonym.getContent()) {
+ writer.startNode(SYNONYM);
+ writeElement(writer, KEY, entry.getLangKey());
+ writeElement(writer, VALUE, entry.getValue());
+ writer.endNode();
+ }
+ }
+
+ private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) {
+ if (text == null) {
+ return;
+ }
+ writer.startNode(nodeName);
+ writer.setValue(text);
+ writer.endNode();
+ }
+
+ /** Конвертер только для записи; чтение не поддерживается. */
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ throw new UnsupportedOperationException("CatalogEdtWriteConverter is for writing only");
+ }
+
+ /** Поддерживается только тип {@link Catalog}. */
+ @Override
+ public boolean canConvert(Class type) {
+ return Catalog.class.isAssignableFrom(type);
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/edt/ConfigurationEdtWriteConverter.java b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/ConfigurationEdtWriteConverter.java
new file mode 100644
index 000000000..d1ff0d509
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/ConfigurationEdtWriteConverter.java
@@ -0,0 +1,232 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer.edt;
+
+import com.github._1c_syntax.bsl.mdo.Language;
+import com.github._1c_syntax.bsl.mdo.MD;
+import com.github._1c_syntax.bsl.mdclasses.Configuration;
+import com.github._1c_syntax.bsl.support.CompatibilityMode;
+import com.github._1c_syntax.bsl.types.MDOType;
+import com.github._1c_syntax.bsl.types.MdoReference;
+import com.github._1c_syntax.bsl.types.MultiLanguageString;
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+import java.util.List;
+
+/**
+ * Конвертер записи конфигурации в формате EDT (Configuration.mdo).
+ */
+public class ConfigurationEdtWriteConverter implements Converter {
+
+ private static final String MDCLASS_NS = "http://g5.1c.ru/v8/dt/metadata/mdclass";
+ private static final String NAME = "name";
+ private static final String KEY = "key";
+ private static final String VALUE = "value";
+ private static final String SYNONYM = "synonym";
+ private static final String LANGUAGE_CODE = "languageCode";
+
+ /** Сериализует конфигурацию в EDT Configuration.mdo (name, synonym, режимы, списки ссылок и т.д.). */
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ var config = (Configuration) source;
+
+ writer.addAttribute("xmlns:mdclass", MDCLASS_NS);
+ if (config.getUuid() != null && !config.getUuid().isEmpty()) {
+ writer.addAttribute("uuid", config.getUuid());
+ }
+
+ writeElement(writer, NAME, config.getName());
+ writeSynonym(writer, config.getSynonym());
+ writeElement(writer, "configurationExtensionCompatibilityMode", compatibilityModeString(config.getConfigurationExtensionCompatibilityMode()));
+ writeElement(writer, "defaultRunMode", config.getDefaultRunMode() != null ? config.getDefaultRunMode().fullName().getEn() : null);
+ if (config.getUsePurposes() != null) {
+ for (var p : config.getUsePurposes()) {
+ writeElement(writer, "usePurposes", p != null ? p.fullName().getEn() : null);
+ }
+ }
+ writeElement(writer, "scriptVariant", config.getScriptVariant() != null ? config.getScriptVariant().nameEn() : null);
+ writeElement(writer, "useManagedFormInOrdinaryApplication", config.isUseManagedFormInOrdinaryApplication() ? "true" : null);
+ writeElement(writer, "useOrdinaryFormInManagedApplication", config.isUseOrdinaryFormInManagedApplication() ? "true" : null);
+ writeMdoRef(writer, "defaultLanguage", config.getDefaultLanguage());
+ writeMultiLang(writer, "briefInformation", config.getBriefInformation());
+ writeMultiLang(writer, "detailedInformation", config.getDetailedInformation());
+ writeMultiLang(writer, "copyright", config.getCopyrights());
+ writeElement(writer, "objectAutonumerationMode", nullToEmpty(config.getObjectAutonumerationMode()));
+ writeElement(writer, "synchronousPlatformExtensionAndAddInCallUseMode",
+ config.getSynchronousPlatformExtensionAndAddInCallUseMode() != null
+ ? config.getSynchronousPlatformExtensionAndAddInCallUseMode().fullName().getEn() : null);
+ writeElement(writer, "compatibilityMode", compatibilityModeString(config.getCompatibilityMode()));
+
+ if (config.getLanguages() != null) {
+ for (var lang : config.getLanguages()) {
+ writeLanguage(writer, lang);
+ }
+ }
+
+ writeRefList(writer, "subsystems", config.getSubsystems());
+ writeRefList(writer, "styleItems", config.getStyleItems());
+ writeRefList(writer, "paletteColors", config.getPaletteColors());
+ writeRefList(writer, "styles", config.getStyles());
+ writeRefList(writer, "commonPictures", config.getCommonPictures());
+ writeRefList(writer, "interfaces", config.getInterfaces());
+ writeRefList(writer, "sessionParameters", config.getSessionParameters());
+ writeRefList(writer, "roles", config.getRoles());
+ writeRefList(writer, "commonTemplates", config.getCommonTemplates());
+ writeRefList(writer, "filterCriteria", config.getFilterCriteria());
+ writeRefList(writer, "commonModules", config.getCommonModules());
+ writeRefList(writer, "commonAttributes", config.getCommonAttributes());
+ writeRefList(writer, "exchangePlans", config.getExchangePlans());
+ writeRefList(writer, "xDTOPackages", config.getXDTOPackages());
+ writeRefList(writer, "webServices", config.getWebServices());
+ writeRefList(writer, "webSocketClients", config.getWebSocketClients());
+ writeRefList(writer, "httpServices", config.getHttpServices());
+ writeRefList(writer, "wsReferences", config.getWsReferences());
+ writeRefList(writer, "integrationServices", config.getIntegrationServices());
+ writeRefList(writer, "eventSubscriptions", config.getEventSubscriptions());
+ writeRefList(writer, "scheduledJobs", config.getScheduledJobs());
+ writeRefList(writer, "bots", config.getBots());
+ writeRefList(writer, "settingsStorages", config.getSettingsStorages());
+ writeRefList(writer, "functionalOptions", config.getFunctionalOptions());
+ writeRefList(writer, "functionalOptionsParameters", config.getFunctionalOptionsParameters());
+ writeRefList(writer, "definedTypes", config.getDefinedTypes());
+ writeRefList(writer, "commonCommands", config.getCommonCommands());
+ writeRefList(writer, "commandGroups", config.getCommandGroups());
+ writeRefList(writer, "constants", config.getConstants());
+ writeRefList(writer, "commonForms", config.getCommonForms());
+ writeRefList(writer, "catalogs", config.getCatalogs());
+ writeRefList(writer, "documents", config.getDocuments());
+ writeRefList(writer, "documentNumerators", config.getDocumentNumerators());
+ writeRefList(writer, "sequences", config.getSequences());
+ writeRefList(writer, "documentJournals", config.getDocumentJournals());
+ writeRefList(writer, "enums", config.getEnums());
+ writeRefList(writer, "reports", config.getReports());
+ writeRefList(writer, "dataProcessors", config.getDataProcessors());
+ writeRefList(writer, "informationRegisters", config.getInformationRegisters());
+ writeRefList(writer, "accumulationRegisters", config.getAccumulationRegisters());
+ writeRefList(writer, "chartsOfCharacteristicTypes", config.getChartsOfCharacteristicTypes());
+ writeRefList(writer, "chartsOfAccounts", config.getChartsOfAccounts());
+ writeRefList(writer, "accountingRegisters", config.getAccountingRegisters());
+ writeRefList(writer, "chartsOfCalculationTypes", config.getChartsOfCalculationTypes());
+ writeRefList(writer, "calculationRegisters", config.getCalculationRegisters());
+ writeRefList(writer, "businessProcesses", config.getBusinessProcesses());
+ writeRefList(writer, "tasks", config.getTasks());
+ writeRefList(writer, "externalDataSources", config.getExternalDataSources());
+ }
+
+ private static String compatibilityModeString(CompatibilityMode mode) {
+ if (mode == null) {
+ return "";
+ }
+ return mode.toString();
+ }
+
+ private static void writeMdoRef(HierarchicalStreamWriter writer, String nodeName, MdoReference ref) {
+ if (ref == null) {
+ return;
+ }
+ String refStr = ref.getMdoRef();
+ if (refStr != null && !refStr.isEmpty()) {
+ writeElement(writer, nodeName, refStr);
+ }
+ }
+
+ private static void writeRefList(HierarchicalStreamWriter writer, String nodeName, List extends MD> list) {
+ if (list == null) {
+ return;
+ }
+ for (var obj : list) {
+ if (obj != null && obj.getName() != null) {
+ var type = obj.getMdoType();
+ if (type != MDOType.UNKNOWN) {
+ writeElement(writer, nodeName, type.nameEn() + "." + obj.getName());
+ }
+ }
+ }
+ }
+
+ private static void writeLanguage(HierarchicalStreamWriter writer, Language lang) {
+ if (lang == null) {
+ return;
+ }
+ writer.startNode("languages");
+ if (lang.getUuid() != null && !lang.getUuid().isEmpty()) {
+ writer.addAttribute("uuid", lang.getUuid());
+ }
+ writeElement(writer, NAME, lang.getName());
+ writeSynonym(writer, lang.getSynonym());
+ writeElement(writer, LANGUAGE_CODE, lang.getLanguageCode());
+ writer.endNode();
+ }
+
+ private static void writeMultiLang(HierarchicalStreamWriter writer, String nodeName, MultiLanguageString multi) {
+ if (multi == null || multi.isEmpty()) {
+ return;
+ }
+ for (var entry : multi.getContent()) {
+ writer.startNode(nodeName);
+ writeElement(writer, KEY, entry.getLangKey());
+ writeElement(writer, VALUE, entry.getValue());
+ writer.endNode();
+ }
+ }
+
+ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageString synonym) {
+ if (synonym == null || synonym.isEmpty()) {
+ return;
+ }
+ for (var entry : synonym.getContent()) {
+ writer.startNode(SYNONYM);
+ writeElement(writer, KEY, entry.getLangKey());
+ writeElement(writer, VALUE, entry.getValue());
+ writer.endNode();
+ }
+ }
+
+ private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) {
+ if (text == null) {
+ return;
+ }
+ writer.startNode(nodeName);
+ writer.setValue(text);
+ writer.endNode();
+ }
+
+ private static String nullToEmpty(String s) {
+ return s != null ? s : "";
+ }
+
+ /** Конвертер только для записи; чтение не поддерживается. */
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ throw new UnsupportedOperationException("ConfigurationEdtWriteConverter is for writing only");
+ }
+
+ /** Поддерживается только тип {@link Configuration}. */
+ @Override
+ public boolean canConvert(Class type) {
+ return Configuration.class.isAssignableFrom(type);
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/edt/EDTWriter.java b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/EDTWriter.java
new file mode 100644
index 000000000..a2dd621b9
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/EDTWriter.java
@@ -0,0 +1,119 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer.edt;
+
+import com.github._1c_syntax.bsl.mdclasses.Configuration;
+import com.github._1c_syntax.bsl.mdclasses.MDCWriteSettings;
+import com.github._1c_syntax.bsl.mdo.Catalog;
+import com.github._1c_syntax.bsl.mdo.Subsystem;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
+import com.thoughtworks.xstream.io.xml.StaxDriver;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+
+/**
+ * Запись объектов метаданных в формате EDT (.mdo).
+ * Поддерживаются типы: Subsystem, Catalog, Configuration.
+ * Запись выполняется во временный файл с последующей атомарной заменой целевого файла.
+ */
+@Slf4j
+public class EDTWriter {
+
+ private final XStream xstream;
+ private final MDCWriteSettings writeSettings;
+
+ /**
+ * Создаёт писатель EDT с заданными настройками.
+ *
+ * @param writeSettings настройки записи (кодировка и др.); null заменяется на {@link MDCWriteSettings#DEFAULT}
+ */
+ public EDTWriter(MDCWriteSettings writeSettings) {
+ this.writeSettings = writeSettings != null ? writeSettings : MDCWriteSettings.DEFAULT;
+ this.xstream = createXStream();
+ }
+
+ /**
+ * Записывает объект в файл .mdo.
+ *
+ * @param path Путь к файлу .mdo (родительские каталоги создаются при необходимости)
+ * @param object Объект метаданных (Subsystem, Catalog или Configuration)
+ * @throws IOException при ошибке записи
+ * @throws IllegalArgumentException если path или object равен null, либо тип object не поддерживается
+ */
+ public void write(Path path, Object object) throws IOException {
+ if (path == null) {
+ throw new IllegalArgumentException("path must not be null");
+ }
+ if (object == null) {
+ throw new IllegalArgumentException("object must not be null");
+ }
+ if (!(object instanceof Subsystem) && !(object instanceof Catalog) && !(object instanceof Configuration)) {
+ throw new IllegalArgumentException(
+ "EDT write supports only Subsystem, Catalog, Configuration, got: " + object.getClass().getName());
+ }
+ Path parent = path.getParent();
+ if (parent != null && !Files.exists(parent)) {
+ Files.createDirectories(parent);
+ }
+ Path tempDir = parent != null ? parent : path.getFileSystem().getPath(".");
+ Path tempFile = Files.createTempFile(tempDir, "mdw", ".mdo");
+ try {
+ var charset = Charset.forName(writeSettings.encoding());
+ try (Writer writer = new OutputStreamWriter(Files.newOutputStream(tempFile), charset)) {
+ writer.write("\n");
+ var prettyWriter = new PrettyPrintWriter(writer);
+ xstream.marshal(object, prettyWriter);
+ }
+ Files.move(tempFile, path, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
+ } finally {
+ if (Files.exists(tempFile)) {
+ try {
+ Files.delete(tempFile);
+ } catch (IOException ignored) {
+ // best effort cleanup
+ }
+ }
+ }
+ }
+
+ private XStream createXStream() {
+ var driver = new StaxDriver();
+ var x = new XStream(driver);
+ x.alias("mdclass:Subsystem", Subsystem.class);
+ x.alias("mdclass:Configuration", Configuration.class);
+ x.alias("mdclass:Catalog", Catalog.class);
+ x.registerConverter(new SubsystemEdtWriteConverter(), XStream.PRIORITY_VERY_HIGH);
+ x.registerConverter(new ConfigurationEdtWriteConverter(), XStream.PRIORITY_VERY_HIGH);
+ x.registerConverter(new CatalogEdtWriteConverter(), XStream.PRIORITY_VERY_HIGH);
+ return x;
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/edt/SubsystemEdtWriteConverter.java b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/SubsystemEdtWriteConverter.java
new file mode 100644
index 000000000..5d7631a3a
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/SubsystemEdtWriteConverter.java
@@ -0,0 +1,115 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer.edt;
+
+import com.github._1c_syntax.bsl.mdo.Subsystem;
+import com.github._1c_syntax.bsl.types.MdoReference;
+import com.github._1c_syntax.bsl.types.MultiLanguageString;
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+/**
+ * Конвертер записи подсистемы в формате EDT (.mdo).
+ */
+public class SubsystemEdtWriteConverter implements Converter {
+
+ private static final String MDCLASS_NS = "http://g5.1c.ru/v8/dt/metadata/mdclass";
+ private static final String NAME = "name";
+ private static final String SYNONYM = "synonym";
+ private static final String KEY = "key";
+ private static final String VALUE = "value";
+ private static final String SUBSYSTEMS = "subsystems";
+ private static final String INCLUDE_IN_COMMAND_INTERFACE = "includeInCommandInterface";
+ private static final String INCLUDE_HELP_IN_CONTENTS = "includeHelpInContents";
+
+ /** Сериализует подсистему в EDT XML (name, synonym, флаги, дочерние подсистемы). */
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ var subsystem = (Subsystem) source;
+
+ if (subsystem.getExplanation() != null && !subsystem.getExplanation().isEmpty()) {
+ throw new IllegalStateException(
+ "EDT Subsystem write does not support non-empty explanation");
+ }
+ if (subsystem.getContent() != null && !subsystem.getContent().isEmpty()) {
+ throw new IllegalStateException(
+ "EDT Subsystem write does not support non-empty content");
+ }
+ if (subsystem.getParentSubsystem() != null && !MdoReference.EMPTY.equals(subsystem.getParentSubsystem())) {
+ throw new IllegalStateException(
+ "EDT Subsystem write does not support non-empty parentSubsystem");
+ }
+
+ writer.addAttribute("xmlns:mdclass", MDCLASS_NS);
+ if (subsystem.getUuid() != null && !subsystem.getUuid().isEmpty()) {
+ writer.addAttribute("uuid", subsystem.getUuid());
+ }
+
+ writeElement(writer, NAME, subsystem.getName());
+ writeSynonym(writer, subsystem.getSynonym());
+ writeElement(writer, INCLUDE_HELP_IN_CONTENTS, subsystem.isIncludeHelpInContents() ? "true" : "false");
+ writeElement(writer, INCLUDE_IN_COMMAND_INTERFACE, subsystem.isIncludeInCommandInterface() ? "true" : "false");
+
+ var children = subsystem.getSubsystems();
+ if (children != null) {
+ for (var child : children) {
+ writeElement(writer, SUBSYSTEMS, child.getName());
+ }
+ }
+ }
+
+ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageString synonym) {
+ if (synonym == null || synonym.isEmpty()) {
+ return;
+ }
+ writer.startNode(SYNONYM);
+ for (var entry : synonym.getContent()) {
+ writeElement(writer, KEY, entry.getLangKey());
+ writeElement(writer, VALUE, entry.getValue());
+ }
+ writer.endNode();
+ }
+
+ private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) {
+ if (text == null) {
+ return;
+ }
+ writer.startNode(nodeName);
+ writer.setValue(text);
+ writer.endNode();
+ }
+
+ /** Конвертер только для записи; чтение не поддерживается. */
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ throw new UnsupportedOperationException("SubsystemEdtWriteConverter is for writing only");
+ }
+
+ /** Поддерживается только тип {@link Subsystem}. */
+ @Override
+ public boolean canConvert(Class type) {
+ return Subsystem.class.isAssignableFrom(type);
+ }
+}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/writer/package-info.java b/src/main/java/com/github/_1c_syntax/bsl/writer/package-info.java
new file mode 100644
index 000000000..14f19c285
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/writer/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+/**
+ * API записи объектов метаданных в форматах EDT (.mdo) и Конфигуратор (Designer .xml).
+ * Фасад: {@link com.github._1c_syntax.bsl.writer.MDOWriter}; настройки:
+ * {@link com.github._1c_syntax.bsl.mdclasses.MDCWriteSettings}.
+ */
+package com.github._1c_syntax.bsl.writer;
diff --git a/src/test/java/com/github/_1c_syntax/bsl/writer/MDOWriterDesignerTest.java b/src/test/java/com/github/_1c_syntax/bsl/writer/MDOWriterDesignerTest.java
new file mode 100644
index 000000000..7ecab2aa4
--- /dev/null
+++ b/src/test/java/com/github/_1c_syntax/bsl/writer/MDOWriterDesignerTest.java
@@ -0,0 +1,152 @@
+/*
+ * This file is a part of MDClasses.
+ *
+ * Copyright (c) 2019 - 2026
+ * Tymko Oleg , Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer;
+
+import com.github._1c_syntax.bsl.mdclasses.Configuration;
+import com.github._1c_syntax.bsl.mdclasses.MDClasses;
+import com.github._1c_syntax.bsl.mdo.Catalog;
+import com.github._1c_syntax.bsl.mdo.Subsystem;
+import com.github._1c_syntax.bsl.types.MultiLanguageString;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Тесты записи объектов метаданных в формате Конфигуратора (Designer .xml).
+ */
+class MDOWriterDesignerTest {
+
+ private static final String META_DATA_OBJECT = "MetaDataObject";
+
+ @Test
+ void writeSubsystemDesignerXml(@TempDir Path tempDir) throws Exception {
+ var subsystem = Subsystem.builder()
+ .name("TestSubsystemDesigner")
+ .uuid("3d00f7d6-e3b0-49cf-8093-e2e4f6ea2293")
+ .synonym(MultiLanguageString.create("ru", "Подсистема для Конфигуратора"))
+ .build();
+
+ var outFile = tempDir.resolve("Subsystems").resolve("TestSubsystemDesigner.xml");
+ MDClasses.writeObject(outFile, subsystem);
+
+ assertThat(outFile).exists().isRegularFile();
+ var content = Files.readString(outFile, StandardCharsets.UTF_8);
+ assertThat(content).contains(META_DATA_OBJECT);
+ assertThat(content).contains("");
+ assertThat(content).contains("");
+ assertThat(content).contains("TestSubsystemDesigner");
+ assertThat(content).contains("Подсистема для Конфигуратора");
+ assertThat(content).contains("");
+ assertThat(content).doesNotContainPattern("]*>\\s*");
+ assertThat(content).contains("ChildSubsystem");
+ assertThat(content).contains("");
+ }
+
+ @Test
+ void writeCatalogDesignerXml(@TempDir Path tempDir) throws Exception {
+ var catalog = Catalog.builder()
+ .name("TestCatalogDesigner")
+ .uuid("eeef463d-d5e7-42f2-ae53-10279661f59d")
+ .synonym(MultiLanguageString.create("ru", "Справочник для Конфигуратора"))
+ .build();
+
+ var outFile = tempDir.resolve("Catalogs").resolve("TestCatalogDesigner.xml");
+ MDClasses.writeObject(outFile, catalog);
+
+ assertThat(outFile).exists().isRegularFile();
+ var content = Files.readString(outFile, StandardCharsets.UTF_8);
+ assertThat(content).contains(META_DATA_OBJECT);
+ assertThat(content).contains("");
+ assertThat(content).contains("");
+ assertThat(content).contains("TestCatalogDesigner");
+ assertThat(content).contains("Справочник для Конфигуратора");
+ assertThat(content).doesNotContainPattern("]*>\\s*");
+ assertThat(content).contains("TestConfigDesigner");
+ assertThat(content).contains("ChildObjects");
+ assertThat(content).doesNotContainPattern("]*>\\s*, Maximov Valery and contributors
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ *
+ * MDClasses is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3.0 of the License, or (at your option) any later version.
+ *
+ * MDClasses is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with MDClasses.
+ */
+package com.github._1c_syntax.bsl.writer;
+
+import com.github._1c_syntax.bsl.mdclasses.Configuration;
+import com.github._1c_syntax.bsl.mdclasses.MDClasses;
+import com.github._1c_syntax.bsl.mdclasses.MDCReadSettings;
+import com.github._1c_syntax.bsl.mdo.Catalog;
+import com.github._1c_syntax.bsl.mdo.Subsystem;
+import com.github._1c_syntax.bsl.reader.MDOReader;
+import com.github._1c_syntax.bsl.reader.edt.EDTReader;
+import com.github._1c_syntax.bsl.types.MultiLanguageString;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Тесты записи объектов метаданных в формате EDT (.mdo).
+ */
+class MDOWriterEdtTest {
+
+ @Test
+ void writeSubsystemThenReadBack(@TempDir Path tempDir) throws Exception {
+ var subsystem = Subsystem.builder()
+ .name("TestSubsystem")
+ .uuid("test-uuid-123")
+ .synonym(MultiLanguageString.create("ru", "Тестовая подсистема"))
+ .build();
+
+ var outFile = tempDir.resolve("Subsystems").resolve("TestSubsystem").resolve("TestSubsystem.mdo");
+ MDClasses.writeObject(outFile, subsystem);
+
+ assertThat(outFile).exists();
+ assertThat(outFile).isRegularFile();
+ var content = Files.readString(outFile, StandardCharsets.UTF_8);
+ assertThat(content).as("written file content").isNotEmpty();
+ assertThat(content).contains("mdclass:Subsystem");
+ assertThat(content).contains("TestSubsystem");
+ assertThat(content).contains("test-uuid-123");
+ assertThat(content).contains("Тестовая подсистема");
+ assertThat(content).contains("");
+ assertThat(content).contains("");
+ int namePos = content.indexOf("TestSubsystem");
+ int includeHelpPos = content.indexOf("");
+ int includeCmdPos = content.indexOf("");
+ assertThat(namePos).isLessThan(includeHelpPos);
+ assertThat(includeHelpPos).isLessThan(includeCmdPos);
+
+ var reader = new EDTReader(outFile, MDCReadSettings.SKIP_SUPPORT);
+ var readBack = reader.read(outFile);
+ assertThat(readBack).isNotNull().isInstanceOf(Subsystem.class);
+ var readSubsystem = (Subsystem) readBack;
+ assertThat(readSubsystem.getName()).isEqualTo(subsystem.getName());
+ assertThat(readSubsystem.getUuid()).isEqualTo(subsystem.getUuid());
+ }
+
+ @Test
+ void writeConfigurationThenReadBack(@TempDir Path tempDir) throws Exception {
+ var config = Configuration.builder()
+ .name("TestConfig")
+ .uuid("test-config-uuid-001")
+ .build();
+
+ var configurationMdo = tempDir.resolve("src").resolve("Configuration").resolve("Configuration.mdo");
+ MDClasses.writeObject(configurationMdo, config);
+
+ assertThat(configurationMdo).exists().isRegularFile();
+ var content = Files.readString(configurationMdo, StandardCharsets.UTF_8);
+ assertThat(content).contains("mdclass:Configuration");
+ assertThat(content).contains("TestConfig");
+ assertThat(content).contains("test-config-uuid-001");
+
+ var readBack = MDOReader.readConfiguration(configurationMdo, MDCReadSettings.SKIP_SUPPORT);
+ assertThat(readBack).isNotNull().isInstanceOf(Configuration.class);
+ }
+
+ @Test
+ void writeCatalogThenReadBack(@TempDir Path tempDir) throws Exception {
+ var catalog = Catalog.builder()
+ .name("TestCatalog")
+ .uuid("catalog-uuid-001")
+ .synonym(MultiLanguageString.create("ru", "Тестовый справочник"))
+ .checkUnique(true)
+ .build();
+
+ var outFile = tempDir.resolve("Catalogs").resolve("TestCatalog").resolve("TestCatalog.mdo");
+ MDClasses.writeObject(outFile, catalog);
+
+ assertThat(outFile).exists().isRegularFile();
+ var content = Files.readString(outFile, StandardCharsets.UTF_8);
+ assertThat(content).contains("Catalog");
+ assertThat(content).contains("TestCatalog");
+ assertThat(content).contains("catalog-uuid-001");
+ assertThat(content).contains("Тестовый справочник");
+
+ var reader = new EDTReader(outFile, MDCReadSettings.SKIP_SUPPORT);
+ var readBack = reader.read(outFile);
+ assertThat(readBack).isNotNull().isInstanceOf(Catalog.class);
+ var readCatalog = (Catalog) readBack;
+ assertThat(readCatalog.getName()).isEqualTo(catalog.getName());
+ assertThat(readCatalog.getUuid()).isEqualTo(catalog.getUuid());
+ }
+
+ @Test
+ void writeObjectThrowsOnNullPath() {
+ var subsystem = Subsystem.builder().name("Test").build();
+ assertThatThrownBy(() -> MDClasses.writeObject((Path) null, subsystem))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void writeObjectThrowsOnNullObject(@TempDir Path tempDir) {
+ var path = tempDir.resolve("Test.mdo");
+ assertThatThrownBy(() -> MDClasses.writeObject(path, null))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void writeObjectThrowsOnUnsupportedFormat(@TempDir Path tempDir) {
+ var subsystem = Subsystem.builder().name("Test").build();
+ var path = tempDir.resolve("Test.txt");
+ assertThatThrownBy(() -> MDClasses.writeObject(path, subsystem))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining(".mdo")
+ .hasMessageContaining(".xml");
+ }
+}