From 5d60e9b26a7e83dc9a1ef89a993209672517e2ec Mon Sep 17 00:00:00 2001 From: Ivan Karlo Date: Fri, 6 Mar 2026 17:40:28 +0300 Subject: [PATCH 1/5] =?UTF-8?q?feat(writeObject):=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BC=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B4=D1=8B=20writeObject=20=D0=B8=20=D0=BA=D0=BB=D0=B0=D1=81?= =?UTF-8?q?=D1=81=20MDCWriteSettings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Введены два перегруженных метода writeObject в MDClasses для записи объектов метаданных в файлы, поддерживающие как настройки записи по умолчанию, так и пользовательские настройки записи. - Добавлен новый класс MDCWriteSettings для инкапсуляции конфигураций записи файлов, включая настройки кодировки по умолчанию. --- .../bsl/mdclasses/MDCWriteSettings.java | 44 +++++++++++++++++++ .../_1c_syntax/bsl/mdclasses/MDClasses.java | 24 ++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDCWriteSettings.java 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..39713bff3 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDCWriteSettings.java @@ -0,0 +1,44 @@ +/* + * 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; + +/** + * Настройки записи MDC + * + * @param encoding Кодировка файлов (по умолчанию UTF-8) + */ +@Builder +public record MDCWriteSettings(String encoding) { + + /** + * Настройки по умолчанию (UTF-8) + */ + public static final MDCWriteSettings DEFAULT = MDCWriteSettings.builder() + .encoding("UTF-8") + .build(); + + public String encoding() { + return encoding != null ? encoding : "UTF-8"; + } +} 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..5c38286be 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,6 +24,7 @@ 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; @@ -66,6 +67,29 @@ public ExternalSource createExternalReport() { return ExternalReport.EMPTY; } + /** + * Записывает объект метаданных в файл (формат определяется по расширению пути: .mdo — EDT). + * + * @param path Путь к файлу (например, .../Subsystems/Name/Name.mdo) + * @param object Объект метаданных (для EDT поддерживается Subsystem) + * @throws IOException при ошибке записи + */ + public void writeObject(Path path, Object object) throws IOException { + MDOWriter.writeObject(path, object); + } + + /** + * Записывает объект метаданных в файл с настройками записи. + * + * @param path Путь к файлу + * @param object Объект метаданных + * @param writeSettings Настройки записи + * @throws IOException при ошибке записи + */ + public void writeObject(Path path, Object object, MDCWriteSettings writeSettings) throws IOException { + MDOWriter.writeObject(path, object, writeSettings); + } + /** * Создает конфигурацию или расширение по указанному пути * From 1603f786841ebdb92664f1173bf43e859f6ab550 Mon Sep 17 00:00:00 2001 From: Ivan Karlo Date: Fri, 6 Mar 2026 17:40:42 +0300 Subject: [PATCH 2/5] =?UTF-8?q?feat(writeObject):=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BB=D0=B0=D1=81?= =?UTF-8?q?=D1=81=D1=8B=20=D0=B8=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=B8=20=D0=BC=D0=B5=D1=82=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2=20=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=D1=85=20EDT=20=D0=B8=20Designer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Создан класс MDOWriter для записи объектов метаданных в файлы форматов EDT (.mdo) и Designer (.xml). - Добавлены конвертеры для записи объектов Catalog и Configuration в формате Designer. - Реализованы конвертеры для записи объектов Catalog и Configuration в формате EDT. - Добавлен класс ReadWriteDemo для демонстрации чтения и записи метаданных. --- .../_1c_syntax/bsl/writer/MDOWriter.java | 77 ++++++ .../_1c_syntax/bsl/writer/ReadWriteDemo.java | 130 ++++++++++ .../CatalogDesignerWriteConverter.java | 120 +++++++++ .../ConfigurationDesignerWriteConverter.java | 169 +++++++++++++ .../bsl/writer/designer/DesignerWriter.java | 100 ++++++++ .../SubsystemDesignerWriteConverter.java | 119 +++++++++ .../writer/edt/CatalogEdtWriteConverter.java | 117 +++++++++ .../edt/ConfigurationEdtWriteConverter.java | 235 ++++++++++++++++++ .../_1c_syntax/bsl/writer/edt/EDTWriter.java | 90 +++++++ .../edt/SubsystemEdtWriteConverter.java | 109 ++++++++ 10 files changed, 1266 insertions(+) create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/MDOWriter.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/ReadWriteDemo.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/designer/CatalogDesignerWriteConverter.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/designer/ConfigurationDesignerWriteConverter.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/designer/DesignerWriter.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/designer/SubsystemDesignerWriteConverter.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/edt/CatalogEdtWriteConverter.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/edt/ConfigurationEdtWriteConverter.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/edt/EDTWriter.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/edt/SubsystemEdtWriteConverter.java 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..345e587a7 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/MDOWriter.java @@ -0,0 +1,77 @@ +/* + * 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 lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FilenameUtils; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Фасад записи объектов метаданных в файлы (EDT .mdo, в перспективе Designer .xml). + */ +@UtilityClass +@Slf4j +public class MDOWriter { + + /** + * Записывает объект метаданных в файл. + * Формат определяется по расширению пути: .mdo — EDT. + * + * @param path Путь к файлу (например, .../Subsystems/Name/Name.mdo) + * @param object Объект метаданных (поддерживается Subsystem для EDT) + * @throws IOException при ошибке записи + * @throws UnsupportedOperationException если формат или тип объекта не поддерживается + */ + public void writeObject(Path path, Object object) throws IOException { + writeObject(path, object, MDCWriteSettings.DEFAULT); + } + + /** + * Записывает объект метаданных в файл с настройками. + * + * @param path Путь к файлу + * @param object Объект метаданных + * @param writeSettings Настройки записи + * @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..8cc064216 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/ReadWriteDemo.java @@ -0,0 +1,130 @@ +/* + * 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() { + } + + 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..830768e42 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/CatalogDesignerWriteConverter.java @@ -0,0 +1,120 @@ +/* + * 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.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; + +/** + * Конвертер записи справочника в формате Конфигуратора (Designer .xml). + * MVP: Name, Synonym, Comment, базовые свойства (UseStandardCommands, LevelCount, CodeLength и т.д.). + */ +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"; + + @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() : ""); + 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", ""); + 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 void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) { + if (text == null) { + return; + } + writer.startNode(nodeName); + writer.setValue(escapeXml(text)); + writer.endNode(); + } + + private static String escapeXml(String s) { + if (s == null) { + return ""; + } + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + throw new UnsupportedOperationException("CatalogDesignerWriteConverter is for writing only"); + } + + @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..38ad244c1 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/ConfigurationDesignerWriteConverter.java @@ -0,0 +1,169 @@ +/* + * 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"; + + @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.getDefinedTypes()); + writeChildList(writer, config.getCommonTemplates()); + writeChildList(writer, config.getCommonPictures()); + writeChildList(writer, config.getCommonAttributes()); + writeChildList(writer, config.getXDTOPackages()); + writeChildList(writer, config.getWebServices()); + writeChildList(writer, config.getHttpServices()); + writeChildList(writer, config.getWsReferences()); + 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()); + writer.endNode(); // ChildObjects + } + + private static void writeChildList(HierarchicalStreamWriter writer, List 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(escapeXml(text)); + writer.endNode(); + } + + private static String escapeXml(String s) { + if (s == null) { + return ""; + } + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + throw new UnsupportedOperationException("ConfigurationDesignerWriteConverter is for writing only"); + } + + @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..425fab6e2 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/DesignerWriter.java @@ -0,0 +1,100 @@ +/* + * 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. + */ +@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; + + public DesignerWriter(MDCWriteSettings writeSettings) { + this.writeSettings = writeSettings != null ? writeSettings : MDCWriteSettings.DEFAULT; + this.xstream = createXStream(); + } + + /** + * Записывает объект в файл .xml (формат Конфигуратора). + * + * @param path Путь к файлу .xml (родительские каталоги создаются при необходимости) + * @param object Объект метаданных (поддерживается Subsystem) + * @throws IOException при ошибке записи + */ + 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..dd1fb7105 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/designer/SubsystemDesignerWriteConverter.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.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; + +/** + * Конвертер записи подсистемы в формате Конфигуратора (Designer .xml). + * Выводит обёртку MetaDataObject и элемент Subsystem с Properties (Name, Synonym, дочерние подсистемы). + */ +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"; + + @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", ""); + writeElement(writer, "Picture", ""); + 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 + } + } + + 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 && !nodeName.equals("Comment") && !nodeName.equals("Explanation") && !nodeName.equals("Picture") && !nodeName.equals("Content")) { + return; + } + writer.startNode(nodeName); + writer.setValue(escapeXml(text != null ? text : "")); + writer.endNode(); + } + + private static String escapeXml(String s) { + if (s == null) { + return ""; + } + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + throw new UnsupportedOperationException("SubsystemDesignerWriteConverter is for writing only"); + } + + @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..9f5f63462 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/CatalogEdtWriteConverter.java @@ -0,0 +1,117 @@ +/* + * 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. + */ +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"; + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + var catalog = (Catalog) source; + + writer.addAttribute("xmlns:mdclass", MDCLASS_NS); + if (catalog.getUuid() != null && !catalog.getUuid().isEmpty()) { + writer.addAttribute("uuid", catalog.getUuid()); + } + + writeElement(writer, NAME, catalog.getName()); + 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; + } + 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(escapeXml(text)); + writer.endNode(); + } + + private static String escapeXml(String s) { + if (s == null) { + return ""; + } + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + throw new UnsupportedOperationException("CatalogEdtWriteConverter is for writing only"); + } + + @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..34b257eeb --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/ConfigurationEdtWriteConverter.java @@ -0,0 +1,235 @@ +/* + * 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"; + + @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()); + 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, "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, "httpServices", config.getHttpServices()); + writeRefList(writer, "wsReferences", config.getWsReferences()); + writeRefList(writer, "eventSubscriptions", config.getEventSubscriptions()); + writeRefList(writer, "scheduledJobs", config.getScheduledJobs()); + 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 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; + } + writer.startNode(SYNONYM); + for (var entry : synonym.getContent()) { + writeElement(writer, KEY, entry.getLangKey()); + writeElement(writer, VALUE, entry.getValue()); + } + writer.endNode(); // close SYNONYM + } + + private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) { + if (text == null) { + return; + } + writer.startNode(nodeName); + writer.setValue(escapeXml(text)); + writer.endNode(); + } + + private static String nullToEmpty(String s) { + return s != null ? s : ""; + } + + private static String escapeXml(String s) { + if (s == null) { + return ""; + } + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + throw new UnsupportedOperationException("ConfigurationEdtWriteConverter is for writing only"); + } + + @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..8980e2a77 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/EDTWriter.java @@ -0,0 +1,90 @@ +/* + * 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; + +/** + * Запись объектов метаданных в формате EDT (.mdo). + */ +@Slf4j +public class EDTWriter { + + private final XStream xstream; + private final MDCWriteSettings writeSettings; + + public EDTWriter(MDCWriteSettings writeSettings) { + this.writeSettings = writeSettings != null ? writeSettings : MDCWriteSettings.DEFAULT; + this.xstream = createXStream(); + } + + /** + * Записывает объект в файл .mdo. + * + * @param path Путь к файлу .mdo (родительские каталоги создаются при необходимости) + * @param object Объект метаданных (например, Subsystem) + * @throws IOException при ошибке записи + */ + public void write(Path path, Object object) throws IOException { + if (object == null) { + throw new IllegalArgumentException("object must not be null"); + } + 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"); + var prettyWriter = new PrettyPrintWriter(writer); + xstream.marshal(object, prettyWriter); + } + } + + 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..b4de0b8ec --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/edt/SubsystemEdtWriteConverter.java @@ -0,0 +1,109 @@ +/* + * 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.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"; + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + var subsystem = (Subsystem) source; + + 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(escapeXml(text)); + writer.endNode(); + } + + private static String escapeXml(String s) { + if (s == null) { + return ""; + } + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + throw new UnsupportedOperationException("SubsystemEdtWriteConverter is for writing only"); + } + + @Override + public boolean canConvert(Class type) { + return Subsystem.class.isAssignableFrom(type); + } +} From 24d5cf8a1290194fded11a468b5441b791302b7c Mon Sep 17 00:00:00 2001 From: Ivan Karlo Date: Fri, 6 Mar 2026 17:40:50 +0300 Subject: [PATCH 3/5] =?UTF-8?q?test(writeObject):=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B8=20=D0=BC=D0=B5=D1=82=D0=B0=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B2=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=D1=85=20EDT=20=D0=B8=20Designer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Созданы тесты MDOWriterDesignerTest и MDOWriterEdtTest для проверки записи объектов метаданных в формате Designer (.xml) и EDT (.mdo). - Тесты включают проверку корректности записанных данных и возможность чтения обратно записанных объектов. - Добавлены проверки на выброс исключений при неверных входных данных. --- .../bsl/writer/MDOWriterDesignerTest.java | 124 ++++++++++++++ .../bsl/writer/MDOWriterEdtTest.java | 159 ++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 src/test/java/com/github/_1c_syntax/bsl/writer/MDOWriterDesignerTest.java create mode 100644 src/test/java/com/github/_1c_syntax/bsl/writer/MDOWriterEdtTest.java 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..c14363a14 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/writer/MDOWriterDesignerTest.java @@ -0,0 +1,124 @@ +/* + * 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 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; + +/** + * Тесты записи объектов метаданных в формате 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(); + try { + MDClasses.writeObject((Path) null, subsystem); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class); + } + } + + @Test + void writeObjectThrowsOnNullObject(@TempDir Path tempDir) { + var path = tempDir.resolve("Test.mdo"); + try { + MDClasses.writeObject(path, null); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class); + } + } + + @Test + void writeObjectThrowsOnUnsupportedFormat(@TempDir Path tempDir) { + var subsystem = Subsystem.builder().name("Test").build(); + var path = tempDir.resolve("Test.txt"); + try { + MDClasses.writeObject(path, subsystem); + } catch (Exception e) { + assertThat(e).isInstanceOf(UnsupportedOperationException.class); + assertThat(e.getMessage()).contains(".mdo").contains(".xml"); + } + } +} From 3c4a54def0647d65cd75f29084657cbd5aa3afd4 Mon Sep 17 00:00:00 2001 From: Ivan Karlo Date: Fri, 6 Mar 2026 21:39:16 +0300 Subject: [PATCH 4/5] =?UTF-8?q?refactor(writeObject):=20=D0=9E=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BB=D0=B0=D1=81?= =?UTF-8?q?=D1=81=D1=8B=20=D0=B8=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D0=B0=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B2=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B0=D1=85=20EDT?= =?UTF-8?q?=20=D0=B8=20Designer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены аннотации @NonNull и @Nullable для параметров методов в классе MDClasses, что улучшает обработку null-значений. - Обновлены комментарии в методах writeObject для более точного описания поддерживаемых форматов и типов объектов. - Введены изменения в класс MDCWriteSettings для поддержки null-значений в параметре кодировки. - Обновлены конвертеры для записи объектов Catalog и Configuration, добавлены новые свойства и улучшена обработка данных. - Реализована атомарная запись в формате EDT с использованием временных файлов для повышения надежности. - Добавлены новые тесты для проверки корректности записи и обработки исключений. --- .../bsl/mdclasses/MDCWriteSettings.java | 19 +++++-- .../_1c_syntax/bsl/mdclasses/MDClasses.java | 12 ++-- .../_1c_syntax/bsl/writer/MDOWriter.java | 18 +++--- .../_1c_syntax/bsl/writer/ReadWriteDemo.java | 5 ++ .../CatalogDesignerWriteConverter.java | 35 +++++++----- .../ConfigurationDesignerWriteConverter.java | 21 +++---- .../bsl/writer/designer/DesignerWriter.java | 11 +++- .../SubsystemDesignerWriteConverter.java | 57 ++++++++++++++----- .../writer/edt/CatalogEdtWriteConverter.java | 32 ++++++----- .../edt/ConfigurationEdtWriteConverter.java | 25 ++++---- .../_1c_syntax/bsl/writer/edt/EDTWriter.java | 47 ++++++++++++--- .../edt/SubsystemEdtWriteConverter.java | 30 ++++++---- .../_1c_syntax/bsl/writer/package-info.java | 27 +++++++++ .../bsl/writer/MDOWriterDesignerTest.java | 28 +++++++++ .../bsl/writer/MDOWriterEdtTest.java | 25 +++----- 15 files changed, 264 insertions(+), 128 deletions(-) create mode 100644 src/main/java/com/github/_1c_syntax/bsl/writer/package-info.java 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 index 39713bff3..1a0584e72 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDCWriteSettings.java +++ b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/MDCWriteSettings.java @@ -22,23 +22,30 @@ package com.github._1c_syntax.bsl.mdclasses; import lombok.Builder; +import org.jspecify.annotations.Nullable; + +import java.nio.charset.StandardCharsets; /** - * Настройки записи MDC + * Настройки записи объектов метаданных в файлы (EDT и Designer). + * Используется при вызове {@link com.github._1c_syntax.bsl.mdclasses.MDClasses#writeObject(java.nio.file.Path, Object, MDCWriteSettings)}. * - * @param encoding Кодировка файлов (по умолчанию UTF-8) + * @param encoding Кодировка записываемых файлов (по умолчанию UTF-8) */ @Builder -public record MDCWriteSettings(String encoding) { +public record MDCWriteSettings(@Nullable String encoding) { /** - * Настройки по умолчанию (UTF-8) + * Настройки по умолчанию: кодировка UTF-8. */ public static final MDCWriteSettings DEFAULT = MDCWriteSettings.builder() - .encoding("UTF-8") + .encoding(StandardCharsets.UTF_8.name()) .build(); + /** + * Возвращает кодировку для записи файлов; при null в настройках возвращается UTF-8. + */ public String encoding() { - return encoding != null ? encoding : "UTF-8"; + 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 5c38286be..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 @@ -28,6 +28,8 @@ 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; @@ -68,13 +70,13 @@ public ExternalSource createExternalReport() { } /** - * Записывает объект метаданных в файл (формат определяется по расширению пути: .mdo — EDT). + * Записывает объект метаданных в файл (формат по расширению пути: .mdo — EDT, .xml — Designer). * - * @param path Путь к файлу (например, .../Subsystems/Name/Name.mdo) - * @param object Объект метаданных (для EDT поддерживается Subsystem) + * @param path Путь к файлу (например, .../Subsystems/Name/Name.mdo или .../Subsystems/Name.xml) + * @param object Объект метаданных (поддерживается Subsystem, Catalog, Configuration) * @throws IOException при ошибке записи */ - public void writeObject(Path path, Object object) throws IOException { + public void writeObject(@NonNull Path path, @NonNull Object object) throws IOException { MDOWriter.writeObject(path, object); } @@ -86,7 +88,7 @@ public void writeObject(Path path, Object object) throws IOException { * @param writeSettings Настройки записи * @throws IOException при ошибке записи */ - public void writeObject(Path path, Object object, MDCWriteSettings 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 index 345e587a7..85ffe9ae0 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/writer/MDOWriter.java +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/MDOWriter.java @@ -25,25 +25,23 @@ import com.github._1c_syntax.bsl.writer.designer.DesignerWriter; import com.github._1c_syntax.bsl.writer.edt.EDTWriter; import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FilenameUtils; import java.io.IOException; import java.nio.file.Path; /** - * Фасад записи объектов метаданных в файлы (EDT .mdo, в перспективе Designer .xml). + * Фасад записи объектов метаданных в файлы (EDT .mdo и Designer .xml). */ @UtilityClass -@Slf4j public class MDOWriter { /** * Записывает объект метаданных в файл. - * Формат определяется по расширению пути: .mdo — EDT. + * Формат определяется по расширению пути: .mdo — EDT, .xml — Designer. * - * @param path Путь к файлу (например, .../Subsystems/Name/Name.mdo) - * @param object Объект метаданных (поддерживается Subsystem для EDT) + * @param path Путь к файлу (например, .../Subsystems/Name/Name.mdo или .../Subsystems/Name.xml) + * @param object Объект метаданных (Subsystem, Catalog, Configuration) * @throws IOException при ошибке записи * @throws UnsupportedOperationException если формат или тип объекта не поддерживается */ @@ -54,10 +52,10 @@ public void writeObject(Path path, Object object) throws IOException { /** * Записывает объект метаданных в файл с настройками. * - * @param path Путь к файлу - * @param object Объект метаданных - * @param writeSettings Настройки записи - * @throws IOException при ошибке записи + * @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 { 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 index 8cc064216..39df03536 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/writer/ReadWriteDemo.java +++ b/src/main/java/com/github/_1c_syntax/bsl/writer/ReadWriteDemo.java @@ -44,6 +44,11 @@ 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"); 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 index 830768e42..e2445a3db 100644 --- 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 @@ -22,8 +22,12 @@ 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; @@ -32,6 +36,8 @@ /** * Конвертер записи справочника в формате Конфигуратора (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 { @@ -44,6 +50,7 @@ public class CatalogDesignerWriteConverter implements Converter { 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; @@ -55,13 +62,14 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC 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", ""); + writeElement(writer, "Owners", formatOwners(catalog.getOwners())); writeElement(writer, "SubordinationUse", "ToItems"); writeElement(writer, "CodeLength", "9"); writeElement(writer, "DescriptionLength", "25"); @@ -88,31 +96,32 @@ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageS 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(escapeXml(text)); + writer.setValue(text); writer.endNode(); } - private static String escapeXml(String s) { - if (s == null) { - return ""; - } - return s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - + /** Конвертер только для записи; чтение не поддерживается. */ @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 index 38ad244c1..931b1c34b 100644 --- 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 @@ -48,6 +48,7 @@ public class ConfigurationDesignerWriteConverter implements Converter { 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; @@ -79,14 +80,18 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC 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()); @@ -106,6 +111,7 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC writeChildList(writer, config.getStyleItems()); writeChildList(writer, config.getLanguages()); writeChildList(writer, config.getCommandGroups()); + writeChildList(writer, config.getPaletteColors()); writer.endNode(); // ChildObjects } @@ -142,26 +148,17 @@ private static void writeElement(HierarchicalStreamWriter writer, String nodeNam return; } writer.startNode(nodeName); - writer.setValue(escapeXml(text)); + writer.setValue(text); writer.endNode(); } - private static String escapeXml(String s) { - if (s == null) { - return ""; - } - return s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - + /** Конвертер только для записи; чтение не поддерживается. */ @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 index 425fab6e2..6f8610c24 100644 --- 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 @@ -39,7 +39,8 @@ /** * Запись объектов метаданных в формате Конфигуратора (Designer .xml). - * Поддерживаются: Subsystem, Catalog, Configuration. + * Поддерживаются только типы: Subsystem, Catalog, Configuration. + * ConfigurationExtension и другие реализации CF в данной версии не поддерживаются. */ @Slf4j public class DesignerWriter { @@ -50,6 +51,11 @@ public class DesignerWriter { 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(); @@ -59,8 +65,9 @@ public DesignerWriter(MDCWriteSettings writeSettings) { * Записывает объект в файл .xml (формат Конфигуратора). * * @param path Путь к файлу .xml (родительские каталоги создаются при необходимости) - * @param object Объект метаданных (поддерживается Subsystem) + * @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) { 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 index dd1fb7105..5d2205c44 100644 --- 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 @@ -29,9 +29,13 @@ 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, дочерние подсистемы). + * Выводит обёртку MetaDataObject и элемент Subsystem с Properties (Name, Synonym, Explanation, дочерние подсистемы). + * Ограничение: элемент Content (состав подсистемы по ссылкам) не выводится — в Designer он в формате xr:Item. */ public class SubsystemDesignerWriteConverter implements Converter { @@ -43,7 +47,11 @@ public class SubsystemDesignerWriteConverter implements Converter { 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; @@ -58,8 +66,9 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC writeElement(writer, "IncludeHelpInContents", subsystem.isIncludeHelpInContents() ? "true" : FALSE); writeElement(writer, "IncludeInCommandInterface", subsystem.isIncludeInCommandInterface() ? "true" : FALSE); writeElement(writer, "UseOneCommand", FALSE); - writeElement(writer, "Explanation", ""); + 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 @@ -73,6 +82,33 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC } } + /** + * Преобразует мультиязычное пояснение в одну строку для элемента 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; @@ -88,30 +124,21 @@ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageS } private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) { - if (text == null && !nodeName.equals("Comment") && !nodeName.equals("Explanation") && !nodeName.equals("Picture") && !nodeName.equals("Content")) { + if (text == null && !ALLOW_EMPTY_NODES.contains(nodeName)) { return; } writer.startNode(nodeName); - writer.setValue(escapeXml(text != null ? text : "")); + writer.setValue(text != null ? text : ""); writer.endNode(); } - private static String escapeXml(String s) { - if (s == null) { - return ""; - } - return s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - + /** Конвертер только для записи; чтение не поддерживается. */ @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 index 9f5f63462..405f73fb8 100644 --- 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 @@ -32,6 +32,8 @@ /** * Конвертер записи справочника в формате EDT (.mdo). * MVP: name, uuid, synonym, checkUnique, codeSeries. + * Часть свойств (useStandardCommands, levelCount, codeLength и т.д.) задаётся значениями по умолчанию, + * так как в модели {@link Catalog} пока нет соответствующих полей; при их появлении нужно перейти на чтение из catalog. */ public class CatalogEdtWriteConverter implements Converter { @@ -41,16 +43,27 @@ public class CatalogEdtWriteConverter implements Converter { 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"); @@ -77,12 +90,12 @@ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageS if (synonym == null || synonym.isEmpty()) { return; } - writer.startNode(SYNONYM); for (var entry : synonym.getContent()) { + writer.startNode(SYNONYM); writeElement(writer, KEY, entry.getLangKey()); writeElement(writer, VALUE, entry.getValue()); + writer.endNode(); } - writer.endNode(); } private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) { @@ -90,26 +103,17 @@ private static void writeElement(HierarchicalStreamWriter writer, String nodeNam return; } writer.startNode(nodeName); - writer.setValue(escapeXml(text)); + writer.setValue(text); writer.endNode(); } - private static String escapeXml(String s) { - if (s == null) { - return ""; - } - return s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - + /** Конвертер только для записи; чтение не поддерживается. */ @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 index 34b257eeb..d1ff0d509 100644 --- 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 @@ -48,6 +48,7 @@ public class ConfigurationEdtWriteConverter implements Converter { 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; @@ -58,6 +59,7 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC } 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) { @@ -86,6 +88,7 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC 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()); @@ -98,10 +101,13 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC 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()); @@ -191,12 +197,12 @@ private static void writeSynonym(HierarchicalStreamWriter writer, MultiLanguageS if (synonym == null || synonym.isEmpty()) { return; } - writer.startNode(SYNONYM); for (var entry : synonym.getContent()) { + writer.startNode(SYNONYM); writeElement(writer, KEY, entry.getLangKey()); writeElement(writer, VALUE, entry.getValue()); + writer.endNode(); } - writer.endNode(); // close SYNONYM } private static void writeElement(HierarchicalStreamWriter writer, String nodeName, String text) { @@ -204,7 +210,7 @@ private static void writeElement(HierarchicalStreamWriter writer, String nodeNam return; } writer.startNode(nodeName); - writer.setValue(escapeXml(text)); + writer.setValue(text); writer.endNode(); } @@ -212,22 +218,13 @@ private static String nullToEmpty(String s) { return s != null ? s : ""; } - private static String escapeXml(String s) { - if (s == null) { - return ""; - } - return s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - + /** Конвертер только для записи; чтение не поддерживается. */ @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 index 8980e2a77..a2dd621b9 100644 --- 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 @@ -36,9 +36,12 @@ 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 { @@ -46,6 +49,11 @@ 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(); @@ -55,24 +63,45 @@ public EDTWriter(MDCWriteSettings writeSettings) { * Записывает объект в файл .mdo. * * @param path Путь к файлу .mdo (родительские каталоги создаются при необходимости) - * @param object Объект метаданных (например, Subsystem) - * @throws IOException при ошибке записи + * @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); } - var charset = Charset.forName(writeSettings.encoding()); - try (Writer writer = new OutputStreamWriter(Files.newOutputStream(path), charset)) { - writer.write("\n"); - var prettyWriter = new PrettyPrintWriter(writer); - xstream.marshal(object, prettyWriter); + 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 + } + } } } 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 index b4de0b8ec..5d7631a3a 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -43,10 +44,24 @@ public class SubsystemEdtWriteConverter implements Converter { 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()); @@ -82,26 +97,17 @@ private static void writeElement(HierarchicalStreamWriter writer, String nodeNam return; } writer.startNode(nodeName); - writer.setValue(escapeXml(text)); + writer.setValue(text); writer.endNode(); } - private static String escapeXml(String s) { - if (s == null) { - return ""; - } - return s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - + /** Конвертер только для записи; чтение не поддерживается. */ @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 index c14363a14..7ecab2aa4 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/writer/MDOWriterDesignerTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/writer/MDOWriterDesignerTest.java @@ -33,6 +33,8 @@ import java.nio.file.Files; import java.nio.file.Path; +import javax.xml.parsers.DocumentBuilderFactory; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -121,4 +123,30 @@ void writeConfigurationDesignerXml(@TempDir Path tempDir) throws Exception { assertThat(content).contains("ChildObjects"); assertThat(content).doesNotContainPattern("]*>\\s* MDClasses.writeObject((Path) null, subsystem)) + .isInstanceOf(IllegalArgumentException.class); } @Test void writeObjectThrowsOnNullObject(@TempDir Path tempDir) { var path = tempDir.resolve("Test.mdo"); - try { - MDClasses.writeObject(path, null); - } catch (Exception e) { - assertThat(e).isInstanceOf(IllegalArgumentException.class); - } + 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"); - try { - MDClasses.writeObject(path, subsystem); - } catch (Exception e) { - assertThat(e).isInstanceOf(UnsupportedOperationException.class); - assertThat(e.getMessage()).contains(".mdo").contains(".xml"); - } + assertThatThrownBy(() -> MDClasses.writeObject(path, subsystem)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining(".mdo") + .hasMessageContaining(".xml"); } } From 68141aabc7cfc623197509908192862e98e45d9c Mon Sep 17 00:00:00 2001 From: Ivan Karlo Date: Wed, 18 Mar 2026 22:06:37 +0300 Subject: [PATCH 5/5] =?UTF-8?q?feat(api):=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20API=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8?= =?UTF-8?q?=20=D0=BC=D0=B5=D1=82=D0=B0=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20BSL=20Language=20Server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Создан новый файл документации api-write.md, описывающий API для записи объектов метаданных в форматах EDT и Designer. - Определены рекомендуемые LSP-методы для чтения и записи конфигураций, включая параметры и ожидаемые ответы. - Добавлен скрипт run-read-write-demo.bat для демонстрации работы API записи метаданных. --- docs/api-write.md | 70 +++++++++++++++++++++++++++++++++ scripts/run-read-write-demo.bat | 5 +++ 2 files changed, 75 insertions(+) create mode 100644 docs/api-write.md create mode 100644 scripts/run-read-write-demo.bat 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 %*