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