From cc30bbe95b713df670a56ddc8fc6965315f88d87 Mon Sep 17 00:00:00 2001 From: theshadowco Date: Wed, 8 Jul 2026 08:21:53 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20Solution=20-=20=D0=BE=D0=B1=D1=8A=D0=B5?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8?= =?UTF-8?q?=20=D1=80=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B2=20=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=8A=D0=B5=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены классы Solution и ObjectProvenance для слияния конфигураций с отслеживанием происхождения объектов. MDClasses.createSolution() теперь возвращает Solution вместо Configuration, содержа объединённую конфигурацию, базовую конфигурацию, список расширений и карту происхождений. - ConfigurationConverter разделён на реализации для каждого формата - Улучшен MDMerger с полной поддержкой дочерних типов: - копирование реквизитов, табличных частей, предопределённых значений для всех типов-владельцев - поддержка регистров, перечислений, задач, расчётов, внешних источников данных, планов счетов - добавлен mergeAll() с возвратом MergeResult и происхождений --- .idea/vcs.xml | 1 - .../_1c_syntax/bsl/mdclasses/MDClasses.java | 136 ++++++----- .../bsl/mdclasses/ObjectProvenance.java | 61 +++++ .../_1c_syntax/bsl/mdclasses/Solution.java | 128 +++++++++++ .../_1c_syntax/bsl/reader/FakeReader.java | 5 - .../_1c_syntax/bsl/reader/MDMerger.java | 157 ++++++++++++- .../_1c_syntax/bsl/reader/MDReader.java | 7 - .../bsl/reader/designer/DesignerReader.java | 5 - .../converter/ConfigurationConverter.java | 34 +-- .../_1c_syntax/bsl/reader/edt/EDTReader.java | 5 - .../edt/converter/ConfigurationConverter.java | 86 +++++++ .../bsl/mdclasses/MDClassesSolutionTest.java | 49 +++- .../bsl/mdclasses/ObjectProvenanceTest.java | 68 ++++++ .../bsl/mdclasses/SolutionTest.java | 108 +++++++++ .../bsl/reader/MDMergerMergeTest.java | 211 ++++++++++++++++++ .../_1c_syntax/bsl/smoke/RightTest.java | 2 +- 16 files changed, 938 insertions(+), 125 deletions(-) create mode 100644 src/main/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenance.java create mode 100644 src/main/java/com/github/_1c_syntax/bsl/mdclasses/Solution.java rename src/main/java/com/github/_1c_syntax/bsl/reader/{common => designer}/converter/ConfigurationConverter.java (70%) create mode 100644 src/main/java/com/github/_1c_syntax/bsl/reader/edt/converter/ConfigurationConverter.java create mode 100644 src/test/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenanceTest.java create mode 100644 src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java create mode 100644 src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 741e395d0..c294ef11d 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,7 +2,6 @@ - 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 988deb480..f2f09b417 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.types.MdoReference; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FilenameUtils; @@ -38,7 +39,10 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; + import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -90,18 +94,6 @@ public MDClass createConfiguration(Path path, MDCReadSettings readSettings) { return MDOReader.readConfiguration(path, readSettings); } - /** - * @param path Путь к корню проекта - * @param skipSupport Флаг управления чтением информации о поддержке - * @return Конфигурация или расширение - * @deprecated Стоит использовать метод с параметром MDCReadSettings. - * Создает конфигурацию или расширение по указанному пути - */ - @Deprecated(since = "0.16.0") - public MDClass createConfiguration(Path path, boolean skipSupport) { - return createConfiguration(path, MDCReadSettings.builder().skipSupport(skipSupport).build()); - } - /** * Создает внешнюю обработку или внешний отчет по указанному пути * @@ -122,19 +114,6 @@ public List createConfigurations(Path sourcePath) { return createConfigurations(sourcePath, MDCReadSettings.DEFAULT); } - /** - * Возвращает список конфигураций\расширений в указанном каталоге исходных файлов - * - * @param sourcePath каталог исходных файлов - * @param skipSupport Флаг управления чтением информации о поддержке - * @return Список прочитанных контейнеров конфигураций и расширений - * @deprecated Стоит использовать метод с параметром MDCReadSettings. - */ - @Deprecated(since = "0.16.0") - public List createConfigurations(Path sourcePath, boolean skipSupport) { - return createConfigurations(sourcePath, MDCReadSettings.builder().skipSupport(skipSupport).build()); - } - /** * Читает каталог проекта и * - возвращает объект MDClass, если содержится только один объект MDC @@ -144,55 +123,57 @@ public List createConfigurations(Path sourcePath, boolean skipSupport) * @param sourcePath Путь к каталогу исходников * @return Результат чтения решения */ - public MDClass createSolution(Path sourcePath) { + public Solution createSolution(Path sourcePath) { return createSolution(sourcePath, MDCReadSettings.DEFAULT); } /** * Читает каталог проекта и - * - возвращает объект MDClass, если содержится только один объект MDC - * - возвращает объединенную конфигурацию с расширениями - * - возвращает объединение расширений с пустой конфигурацией + * - возвращает Solution с единственной конфигурацией, если содержится только один объект MDC + * - возвращает Solution с объединенной конфигурацией и расширениями + * - возвращает Solution с объединением расширений с пустой конфигурацией * * @param sourcePath Путь к каталогу исходников * @param readSettings Настройки чтения проекта * @return Результат чтения решения */ - public MDClass createSolution(Path sourcePath, MDCReadSettings readSettings) { + public Solution createSolution(Path sourcePath, MDCReadSettings readSettings) { var mdcs = createConfigurations(sourcePath, readSettings); - MDClass result; + if (mdcs.isEmpty()) { - result = Configuration.EMPTY; - } else if (mdcs.size() == 1) { - result = mdcs.getFirst(); - } else { - var mdc = mdcs.stream().filter(Configuration.class::isInstance).map(Configuration.class::cast).findFirst(); - var cf = mdc.orElse(Configuration.EMPTY); - var extensions = mdcs.stream() - .filter(ConfigurationExtension.class::isInstance) - .map(ConfigurationExtension.class::cast) - .toList(); - - if (cf.isEmpty()) { - if (extensions.isEmpty()) { - // вернем первое значение, т.к. там нет ни конфы, ни расширений - return mdcs.getFirst(); - } else if (extensions.size() == 1) { - // есть одно расширение, вернем его - return extensions.getFirst(); - } - } else if (extensions.isEmpty()) { - // расширений нет, вернем конфигурацию - return cf; - } + return Solution.EMPTY; + } - // объединим расширения с конфигурацией в одно целое - result = cf; - for (var extension : extensions) { - result = MDMerger.merge((Configuration) result, extension); + if (mdcs.size() == 1) { + var mdc = mdcs.getFirst(); + if (mdc instanceof Configuration cf) { + return buildSolution(cf, Collections.emptyList()); + } + if (mdc instanceof ConfigurationExtension ext) { + return buildSolution(Configuration.EMPTY, List.of(ext)); } + + // заглушка, если получилось что-то странное + return Solution.EMPTY; } - return result; + + // решение содержит несколько компонент, разделяем + var base = mdcs.stream() + .filter(Configuration.class::isInstance) + .map(Configuration.class::cast) + .findFirst() + .orElse(Configuration.EMPTY); + + var extensions = mdcs.stream() + .filter(ConfigurationExtension.class::isInstance) + .map(ConfigurationExtension.class::cast) + .toList(); + + // что-то странное прочли + if (base.isEmpty() && extensions.isEmpty()) { + return Solution.EMPTY; + } + return buildSolution(base, extensions); } /** @@ -241,19 +222,6 @@ public List create(Path sourcePath) { return create(sourcePath, MDCReadSettings.DEFAULT); } - /** - * Возвращает список контейнеров метаданных в указанном каталоге исходных файлов - * - * @param sourcePath каталог исходных файлов - * @param skipSupport Флаг управления чтением информации о поддержке - * @return Список прочитанных контейнеров - * @deprecated Стоит использовать метод с параметром MDCReadSettings. - */ - @Deprecated(since = "0.16.0") - public List create(Path sourcePath, boolean skipSupport) { - return create(sourcePath, MDCReadSettings.builder().skipSupport(skipSupport).build()); - } - /** * Возвращает список контейнеров метаданных в указанном каталоге исходных файлов * @@ -326,4 +294,28 @@ private Set mdoTypeGroupNames() { .map(MDOType::groupName) .collect(Collectors.toSet()); } + + private static Solution buildSolution(Configuration base, List extensions) { + if (extensions.isEmpty()) { + var provenance = new HashMap(); + base.getChildrenByMdoRef().forEach((ref, md) -> + provenance.put(ref, ObjectProvenance.builder() + .ownerRef(base.getMdoReference()) + .objectBelonging(md.getObjectBelonging()) + .build())); + return Solution.builder() + .mergedConfiguration(base) + .baseConfiguration(base) + .provenance(Collections.unmodifiableMap(provenance)) + .build(); + } + + var mergeResult = MDMerger.mergeAll(base, extensions); + return Solution.builder() + .mergedConfiguration(mergeResult.configuration()) + .baseConfiguration(base) + .extensions(extensions) + .provenance(mergeResult.provenance()) + .build(); + } } diff --git a/src/main/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenance.java b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenance.java new file mode 100644 index 000000000..6ca9a549b --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenance.java @@ -0,0 +1,61 @@ +/* + * 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 com.github._1c_syntax.bsl.mdo.support.ObjectBelonging; +import com.github._1c_syntax.bsl.reader.MDMerger; +import com.github._1c_syntax.bsl.types.MdoReference; +import lombok.Builder; +import lombok.Singular; +import lombok.Value; + +import java.util.List; + +/** + * Хранит информацию о происхождении объекта метаданных в составе {@link Solution}. + *

+ * Заполняется автоматически в {@link MDMerger#mergeAll(Configuration, java.util.List)} + * при построении решения. + * + */ +@Value +@Builder +public class ObjectProvenance { + + /** + * Конфигурация или расширение, которая является владельцем объекта + */ + @Builder.Default + MdoReference ownerRef = MdoReference.EMPTY; + + /** + * Расширения (если есть), которые модифицировали объект + */ + @Singular + List modifiedByExtensionRefs; + + /** + * Признак собственного или заимствованного (унаследованного) объекта. + */ + @Builder.Default + ObjectBelonging objectBelonging = ObjectBelonging.OWN; +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/mdclasses/Solution.java b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/Solution.java new file mode 100644 index 000000000..b6de7d357 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/Solution.java @@ -0,0 +1,128 @@ +/* + * 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 com.github._1c_syntax.bsl.mdo.support.RoleRight; +import com.github._1c_syntax.bsl.types.MdoReference; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Delegate; +import org.jspecify.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Представляет результат разрешения конфигурации — результат слияния основной конфигурации + * с нулем или более расширениями. + *

+ * Создаётся через {@link MDClasses#createSolution(java.nio.file.Path)} и является + * основным входным классом для потребителей, которым требуется конфигурация с поддержкой расширений. + * + */ +@Value +@Builder +public class Solution implements CF { + + /** + * Пустое решение (нет объектов метаданных, нет расширений) + */ + public static final Solution EMPTY = new Solution( + Configuration.EMPTY, Configuration.EMPTY, Collections.emptyList(), Map.of()); + + /** + * Объединенная конфигурация — результат слияния основной конфигурации со всеми расширениями + */ + @Delegate + Configuration mergedConfiguration; + + /** + * Оригинальная конфигурация без расширений + */ + @Builder.Default + Configuration baseConfiguration = Configuration.EMPTY; + + /** + * Список расширений, примененных к базовой конфигурации + */ + @Builder.Default + List extensions = Collections.emptyList(); + + /** + * Хранит, какой конфигурации или расширению принадлежит объект и какими расширениями + * был изменен. + */ + Map provenance; + + /** + * Возвращает принадлежность для заданной ссылки на объект метаданных. + * + * @param mdoRef ссылка на объект метаданных в объединенной конфигурации + * @return запись о происхождении, или {@code null}, если объект не отслеживается + */ + @Nullable + public ObjectProvenance getProvenance(MdoReference mdoRef) { + return provenance.get(mdoRef); + } + + /** + * Возвращает ссылку на конфигурацию или расширение, которому принадлежит + * указанный объект метаданных. + * + * @param mdoRef ссылка на объект метаданных в объединенной конфигурации + * @return ссылка на владельца, или {@link MdoReference#EMPTY}, если не найден + */ + public MdoReference getOwnerRef(MdoReference mdoRef) { + var prov = provenance.get(mdoRef); + return prov != null ? prov.getOwnerRef() : MdoReference.EMPTY; + } + + /** + * Возвращает конфигурацию или расширение ({@link MDClass}), которому принадлежит + * указанный объект метаданных. + *

+ * Поиск выполняется сначала в базовой конфигурации, затем последовательно в + * каждом расширении. + * + * @param mdoRef ссылка на объект метаданных в эффективной конфигурации + * @return владелец {@link MDClass}, или {@code null}, если владелец не найден + */ + @Nullable + public MDClass getOwner(MdoReference mdoRef) { + var ownerRef = getOwnerRef(mdoRef); + if (ownerRef.equals(baseConfiguration.getMdoReference())) { + return baseConfiguration; + } + return extensions.stream() + .filter(ext -> ext.getMdoReference().equals(ownerRef)) + .findFirst() + .orElse(null); + } + + /** + * Возвращает перечень возможных прав доступа + */ + public static List possibleRights() { + return Configuration.possibleRights(); + } +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/FakeReader.java b/src/main/java/com/github/_1c_syntax/bsl/reader/FakeReader.java index 5035d51a7..ec2ab49ae 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/FakeReader.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/FakeReader.java @@ -112,11 +112,6 @@ public String subsystemsNodeName() { return ""; } - @Override - public String configurationExtensionFilter() { - return ""; - } - @Override public void unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context, diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java b/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java index d74ff6123..b4b57c935 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java @@ -24,23 +24,46 @@ import com.github._1c_syntax.bsl.mdclasses.CF; import com.github._1c_syntax.bsl.mdclasses.Configuration; import com.github._1c_syntax.bsl.mdclasses.ConfigurationExtension; +import com.github._1c_syntax.bsl.mdclasses.ObjectProvenance; +import com.github._1c_syntax.bsl.mdo.Attribute; +import com.github._1c_syntax.bsl.mdo.CalculationRegister; +import com.github._1c_syntax.bsl.mdo.ChartOfAccounts; import com.github._1c_syntax.bsl.mdo.ChildrenOwner; import com.github._1c_syntax.bsl.mdo.Command; import com.github._1c_syntax.bsl.mdo.CommandOwner; +import com.github._1c_syntax.bsl.mdo.Enum; +import com.github._1c_syntax.bsl.mdo.ExternalDataSource; import com.github._1c_syntax.bsl.mdo.Form; import com.github._1c_syntax.bsl.mdo.FormOwner; import com.github._1c_syntax.bsl.mdo.MD; import com.github._1c_syntax.bsl.mdo.Module; import com.github._1c_syntax.bsl.mdo.ModuleOwner; +import com.github._1c_syntax.bsl.mdo.PredefinedDataOwner; +import com.github._1c_syntax.bsl.mdo.ReferenceObject; +import com.github._1c_syntax.bsl.mdo.Register; +import com.github._1c_syntax.bsl.mdo.TabularSection; +import com.github._1c_syntax.bsl.mdo.TabularSectionOwner; +import com.github._1c_syntax.bsl.mdo.Task; import com.github._1c_syntax.bsl.mdo.Template; import com.github._1c_syntax.bsl.mdo.TemplateOwner; +import com.github._1c_syntax.bsl.mdo.children.EnumValue; +import com.github._1c_syntax.bsl.mdo.children.ExternalDataSourceCube; +import com.github._1c_syntax.bsl.mdo.children.ExternalDataSourceCubeDimensionTable; +import com.github._1c_syntax.bsl.mdo.children.ExternalDataSourceFunction; +import com.github._1c_syntax.bsl.mdo.children.ExternalDataSourceTable; +import com.github._1c_syntax.bsl.mdo.children.PredefinedValue; +import com.github._1c_syntax.bsl.mdo.children.Recalculation; +import com.github._1c_syntax.bsl.mdo.children.TaskAddressingAttribute; import com.github._1c_syntax.bsl.reader.common.TransformationUtils; +import com.github._1c_syntax.bsl.types.MdoReference; import lombok.experimental.UtilityClass; import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -59,7 +82,6 @@ public class MDMerger { */ public static Configuration merge(Configuration cf, ConfigurationExtension extension) { var builder = cf.toBuilder(); - // todo подумать о том, как контролировать, что все свойства копируются if (cf.isEmpty()) { // скопируем из первого расширения builder.configurationSource(extension.getConfigurationSource()) .name("solution") @@ -131,6 +153,27 @@ public static Configuration merge(Configuration cf, ConfigurationExtension exten .build(); } + /** + * Последовательное слияние базовой конфигурации со списком расширений. + * Каждое следующее расширение применяется к результату предыдущего. + * + * @param base Базовая конфигурация + * @param extensions Список расширений (в порядке применения) + * @return MergeResult с конфигурацией и происхождениями + */ + public static MergeResult mergeAll(Configuration base, List extensions) { + var provenance = new HashMap(); + initProvenance(base, base.getMdoReference(), provenance); + + var result = base; + for (var ext : extensions) { + initProvenance(ext, ext.getMdoReference(), provenance); + result = merge(result, ext); + } + + return new MergeResult(result, Collections.unmodifiableMap(provenance)); + } + /** * Объединяет список модулей. Выполняется простое добавление модулей из копируемого объекта, т.к. в любом случае * совпадений быть не может (модуль связан с файлом на диске) @@ -253,8 +296,6 @@ private static T copyModuleOwner(T srcMD, T modMD) { @SuppressWarnings("unchecked") private static T copyChildrenOwner(T srcMD, T modMD) { - // todo доделать копирование атрибутов и других дочерних - var result = srcMD; if (result instanceof ChildrenOwner childrenOwner) { Object builder = null; @@ -265,6 +306,16 @@ private static T copyChildrenOwner(T srcMD, T modMD) { child -> !(child instanceof Form) && !(child instanceof Template) && !(child instanceof Command) + && !(child instanceof Attribute) + && !(child instanceof TabularSection) + && !(child instanceof PredefinedValue) + && !(child instanceof EnumValue) + && !(child instanceof Recalculation) + && !(child instanceof TaskAddressingAttribute) + && !(child instanceof ExternalDataSourceTable) + && !(child instanceof ExternalDataSourceCube) + && !(child instanceof ExternalDataSourceFunction) + && !(child instanceof ExternalDataSourceCubeDimensionTable) ).toList() ); @@ -280,6 +331,82 @@ private static T copyChildrenOwner(T srcMD, T modMD) { builder = copyCommandOwner(commandOwner, (CommandOwner) modMD, builder, newChildren); } + // ReferenceObject - attributes (Catalog, Document, ChartOfAccounts, Task, etc.) + if (srcMD instanceof ReferenceObject referenceObject) { + builder = copyChildrenList(referenceObject, (ReferenceObject) modMD, builder, + ReferenceObject::getAttributes, "attributes", newChildren); + } + + // TabularSectionOwner - tabular sections + if (srcMD instanceof TabularSectionOwner tabularSectionOwner) { + builder = copyChildrenList(tabularSectionOwner, (TabularSectionOwner) modMD, builder, + TabularSectionOwner::getTabularSections, "tabularSections", newChildren); + } + + // PredefinedDataOwner - predefined values + if (srcMD instanceof PredefinedDataOwner predefinedDataOwner) { + builder = copyChildrenList(predefinedDataOwner, (PredefinedDataOwner) modMD, builder, + PredefinedDataOwner::getPredefinedValues, "predefinedValues", newChildren); + } + + // Register - attributes, resources, dimensions + if (srcMD instanceof Register register) { + builder = copyChildrenList(register, (Register) modMD, builder, + Register::getAttributes, "attributes", newChildren); + builder = copyChildrenList(register, (Register) modMD, builder, + Register::getResources, "resources", newChildren); + builder = copyChildrenList(register, (Register) modMD, builder, + Register::getDimensions, "dimensions", newChildren); + } + + // Enum - attributes, enumValues + if (srcMD instanceof Enum anEnum) { + builder = copyChildrenList(anEnum, (Enum) modMD, builder, + Enum::getAttributes, "attributes", newChildren); + builder = copyChildrenList(anEnum, (Enum) modMD, builder, + Enum::getEnumValues, "enumValues", newChildren); + } + + // Task - addressing attributes + if (srcMD instanceof Task task) { + builder = copyChildrenList(task, (Task) modMD, builder, + Task::getAddressingAttributes, "addressingAttributes", newChildren); + } + + // CalculationRegister - recalculations + if (srcMD instanceof CalculationRegister calculationRegister) { + builder = copyChildrenList(calculationRegister, (CalculationRegister) modMD, builder, + CalculationRegister::getRecalculations, "recalculations", newChildren); + } + + // ExternalDataSource - tables, cubes, functions + if (srcMD instanceof ExternalDataSource externalDataSource) { + builder = copyChildrenList(externalDataSource, (ExternalDataSource) modMD, builder, + ExternalDataSource::getTables, "tables", newChildren); + builder = copyChildrenList(externalDataSource, (ExternalDataSource) modMD, builder, + ExternalDataSource::getCubes, "cubes", newChildren); + builder = copyChildrenList(externalDataSource, (ExternalDataSource) modMD, builder, + ExternalDataSource::getFunctions, "functions", newChildren); + } + + // ExternalDataSourceCube - dimensionTables, dimensions, resources + if (srcMD instanceof ExternalDataSourceCube externalDataSourceCube) { + builder = copyChildrenList(externalDataSourceCube, (ExternalDataSourceCube) modMD, builder, + ExternalDataSourceCube::getDimensionTables, "dimensionTables", newChildren); + builder = copyChildrenList(externalDataSourceCube, (ExternalDataSourceCube) modMD, builder, + ExternalDataSourceCube::getDimensions, "dimensions", newChildren); + builder = copyChildrenList(externalDataSourceCube, (ExternalDataSourceCube) modMD, builder, + ExternalDataSourceCube::getResources, "resources", newChildren); + } + + // ChartOfAccounts - accountingFlags, extDimensionAccountingFlags + if (srcMD instanceof ChartOfAccounts chartOfAccounts) { + builder = copyChildrenList(chartOfAccounts, (ChartOfAccounts) modMD, builder, + ChartOfAccounts::getAccountingFlags, "accountingFlags", newChildren); + builder = copyChildrenList(chartOfAccounts, (ChartOfAccounts) modMD, builder, + ChartOfAccounts::getExtDimensionAccountingFlags, "extDimensionAccountingFlags", newChildren); + } + if (builder != null) { TransformationUtils.invoke(builder, "clearChildren"); TransformationUtils.setValue(builder, "children", newChildren); @@ -332,9 +459,27 @@ private static Object copyTemplateOwner(TemplateOwner srcMD, @Nullable private static Object copyCommandOwner(CommandOwner srcMD, - CommandOwner modMD, - @Nullable Object builder, - ArrayList newChildren) { + CommandOwner modMD, + @Nullable Object builder, + ArrayList newChildren) { return copyChildrenList(srcMD, modMD, builder, CommandOwner::getCommands, "Commands", newChildren); } + + private static void initProvenance(CF cf, MdoReference ownerRef, + Map provenance) { + cf.getChildrenByMdoRef().forEach((MdoReference ref, MD md) -> { + if (!provenance.containsKey(ref)) { + provenance.put(ref, ObjectProvenance.builder() + .ownerRef(ownerRef) + .objectBelonging(md.getObjectBelonging()) + .build()); + } + }); + } + + /** + * Результат слияния: конфигурация и происхождения объектов. + */ + public record MergeResult(Configuration configuration, Map provenance) {} + } diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/MDReader.java b/src/main/java/com/github/_1c_syntax/bsl/reader/MDReader.java index ca2095a8d..5cdcd9982 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/MDReader.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/MDReader.java @@ -177,13 +177,6 @@ default List readPredefinedData(Path currentPath, String name, */ String subsystemsNodeName(); - /** - * Возвращает строку-фильтр для различия расширения и конфигурации - * - * @return Строка-фильтр - */ - String configurationExtensionFilter(); - /** * Выполняет чтение объекта из файла * diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/designer/DesignerReader.java b/src/main/java/com/github/_1c_syntax/bsl/reader/designer/DesignerReader.java index d6381cd51..a3ee85605 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/designer/DesignerReader.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/designer/DesignerReader.java @@ -231,11 +231,6 @@ public String subsystemsNodeName() { return "Subsystem"; } - @Override - public String configurationExtensionFilter() { - return "()"; - } - @Override public void unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context, diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/common/converter/ConfigurationConverter.java b/src/main/java/com/github/_1c_syntax/bsl/reader/designer/converter/ConfigurationConverter.java similarity index 70% rename from src/main/java/com/github/_1c_syntax/bsl/reader/common/converter/ConfigurationConverter.java rename to src/main/java/com/github/_1c_syntax/bsl/reader/designer/converter/ConfigurationConverter.java index f0887e986..d1625823a 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/common/converter/ConfigurationConverter.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/designer/converter/ConfigurationConverter.java @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Lesser General Public * License along with MDClasses. */ -package com.github._1c_syntax.bsl.reader.common.converter; +package com.github._1c_syntax.bsl.reader.designer.converter; import com.github._1c_syntax.bsl.mdclasses.Configuration; import com.github._1c_syntax.bsl.mdclasses.ConfigurationExtension; @@ -29,29 +29,22 @@ import com.github._1c_syntax.bsl.reader.common.xstream.ReadConverter; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; -import lombok.SneakyThrows; -import java.io.FileInputStream; -import java.nio.charset.StandardCharsets; -import java.util.Scanner; +import java.io.IOException; +import java.nio.file.Files; /** - * Обобщенный конвертер для контейнеров конфигурации и расширения + * Конвертер для конфигурации/расширения в формате Конфигуратора (Designer). + * Определяет тип по наличию строки {@code ConfigurationExtensionPurpose} в файле */ -@CommonConverter +@DesignerConverter public class ConfigurationConverter implements ReadConverter { - @SneakyThrows @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { - int count; - var fileInputStream = new FileInputStream(ExtendXStream.getCurrentPath(reader).toFile()); - try (var scanner = new Scanner(fileInputStream, StandardCharsets.UTF_8)) { - count = (int) scanner.findAll(ExtendXStream.getCurrentMDReader(reader).configurationExtensionFilter()).count(); - } - Class realClass = Configuration.class; - if (count > 0) { + + if (hasExtensionMarker(reader)) { realClass = ConfigurationExtension.class; } @@ -64,4 +57,15 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co public boolean canConvert(Class type) { return ConfigurationTree.class.isAssignableFrom(type); } + + /** + * Проверяет наличие строки ConfigurationExtensionPurpose в файле + */ + private static boolean hasExtensionMarker(HierarchicalStreamReader reader) { + try (var lines = Files.lines(ExtendXStream.getCurrentPath(reader))) { + return lines.anyMatch(line -> line.contains("ConfigurationExtensionPurpose")); + } catch (IOException e) { + return false; + } + } } diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/edt/EDTReader.java b/src/main/java/com/github/_1c_syntax/bsl/reader/edt/EDTReader.java index 43ff32d62..3a4928f80 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/edt/EDTReader.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/edt/EDTReader.java @@ -231,11 +231,6 @@ public String subsystemsNodeName() { return "subsystems"; } - @Override - public String configurationExtensionFilter() { - return "()"; - } - @Override public void unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context, diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/edt/converter/ConfigurationConverter.java b/src/main/java/com/github/_1c_syntax/bsl/reader/edt/converter/ConfigurationConverter.java new file mode 100644 index 000000000..ed59e6180 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/edt/converter/ConfigurationConverter.java @@ -0,0 +1,86 @@ +/* + * 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.reader.edt.converter; + +import com.github._1c_syntax.bsl.mdclasses.Configuration; +import com.github._1c_syntax.bsl.mdclasses.ConfigurationExtension; +import com.github._1c_syntax.bsl.mdclasses.ConfigurationTree; +import com.github._1c_syntax.bsl.reader.common.context.MDCReaderContext; +import com.github._1c_syntax.bsl.reader.common.xstream.ExtendReaderWrapper; +import com.github._1c_syntax.bsl.reader.common.xstream.ReadConverter; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; + +/** + * Конвертер для конфигурации/расширения в формате EDT. + * Определяет тип по атрибутам и namespace корневого элемента + */ +@EDTConverter +public class ConfigurationConverter implements ReadConverter { + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + Class realClass = Configuration.class; + + if (hasExtensionMarker(reader)) { + realClass = ConfigurationExtension.class; + } + + var readerContext = new MDCReaderContext(realClass, reader); + readerContext.getMdReader().unmarshal(reader, context, readerContext); + return readerContext.build(); + } + + @Override + public boolean canConvert(Class type) { + return ConfigurationTree.class.isAssignableFrom(type); + } + + /** + * Проверяет EDT-формат: {@code xsi:type="mdclassExtension:ConfigurationExtension"} + * или атрибут {@code extension="true"} в корневом элементе, + * или наличие namespace {@code xmlns:mdclassExtension}. + */ + private static boolean hasExtensionMarker(HierarchicalStreamReader reader) { + var typeAttr = reader.getAttribute("xsi:type"); + if (typeAttr != null && typeAttr.contains("mdclassExtension:ConfigurationExtension")) { + return true; + } + + var extAttr = reader.getAttribute("extension"); + if ("true".equals(extAttr)) { + return true; + } + + // Проверка через namespace (некоторые форматы EDT не выставляют xsi:type на корне) + if (reader instanceof ExtendReaderWrapper wrapper) { + var xmlReader = wrapper.getXMLStreamReader(); + for (int i = 0; i < xmlReader.getNamespaceCount(); i++) { + if ("mdclassExtension".equals(xmlReader.getNamespacePrefix(i))) { + return true; + } + } + } + + return false; + } +} diff --git a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/MDClassesSolutionTest.java b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/MDClassesSolutionTest.java index 0fd2ece47..283fa2d7e 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/MDClassesSolutionTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/MDClassesSolutionTest.java @@ -41,30 +41,63 @@ class MDClassesSolutionTest { @Test void createSolutionEmpty() { var solutionEmpty = MDClasses.createSolution(Path.of("src/test/resources/fixtures")); - assertThat(solutionEmpty).isEqualTo(Configuration.EMPTY); + assertThat(solutionEmpty).isInstanceOf(Solution.class); + assertThat(solutionEmpty.getMergedConfiguration()).isEqualTo(Configuration.EMPTY); } @ParameterizedTest @CsvSource( { "src/test/resources/ext/designer/mdclasses/src/cf", - "src/test/resources/ext/edt/mdclasses_3_27/configuration", - "src/test/resources/ext/designer/mdclasses_ext/src/cf" + "src/test/resources/ext/edt/mdclasses_3_27/configuration" } ) void createSolutionSimple(ArgumentsAccessor argumentsAccessor) { var path = Path.of(argumentsAccessor.getString(0)); var solutionCf = MDClasses.createSolution(path); var cf = MDClasses.createConfigurations(path).get(0); - Assertions.assertThat(Fixtures.asJson(solutionCf), true) + Assertions.assertThat(Fixtures.asJson(solutionCf.getMergedConfiguration()), true) .isEqual(Fixtures.asJson(cf)); } + @ParameterizedTest + @CsvSource( + { + "false, mdclasses_ext", + "true, mdclasses_ext" + } + ) + void createSolutionSimpleExtension(ArgumentsAccessor argumentsAccessor) { + var isEDT = argumentsAccessor.getBoolean(0); + var pack = argumentsAccessor.getString(1); + + var basePath = isEDT + ? Path.of("src/test/resources/ext/edt/" + pack + "/configuration") + : Path.of("src/test/resources/ext/designer/" + pack + "/src/cf"); + + var solution = MDClasses.createSolution(basePath); + assertThat(solution.getMergedConfiguration()).isNotNull(); + assertThat(solution.getBaseConfiguration()).isEqualTo(Configuration.EMPTY); + assertThat(solution.getExtensions()).hasSize(1); + } + + @Test + void extensionFormatsProduceIdenticalResult() { + var desPath = Path.of("src/test/resources/ext/designer/mdclasses_ext/src/cf"); + var edtPath = Path.of("src/test/resources/ext/edt/mdclasses_ext/configuration"); + + var desSolution = MDClasses.createSolution(desPath); + var edtSolution = MDClasses.createSolution(edtPath); + + Assertions.assertThat(Fixtures.asJson(desSolution.getMergedConfiguration()), true) + .isEqual(Fixtures.asJson(edtSolution.getMergedConfiguration())); + } + @Test void createSolutionCf_2_exts() { var solution = MDClasses.createSolution(Path.of("src/test/resources/solutions/sol1")); - assertThat(solution).isInstanceOf(Configuration.class); - var cf = (Configuration) solution; + assertThat(solution).isInstanceOf(Solution.class); + var cf = solution.getMergedConfiguration(); assertThat(cf.getSupportVariant()).isEqualTo(SupportVariant.NONE); assertThat(cf.getModules()).hasSize(3); assertThat(cf.getChildren()).hasSize(12); @@ -94,8 +127,8 @@ void createSolutionCf_2_exts() { @Test void createSolutionCf_2_exts_empty_cf() { var solution = MDClasses.createSolution(Path.of("src/test/resources/solutions/sol2")); - assertThat(solution).isInstanceOf(Configuration.class); - var cf = (Configuration) solution; + assertThat(solution).isInstanceOf(Solution.class); + var cf = solution.getMergedConfiguration(); assertThat(cf.getSupportVariant()).isEqualTo(SupportVariant.NONE); assertThat(cf.getModules()).hasSize(1); assertThat(cf.getChildren()).hasSize(8); diff --git a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenanceTest.java b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenanceTest.java new file mode 100644 index 000000000..b82a3b4bb --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenanceTest.java @@ -0,0 +1,68 @@ +/* + * 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 com.github._1c_syntax.bsl.mdo.support.ObjectBelonging; +import com.github._1c_syntax.bsl.types.MdoReference; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ObjectProvenanceTest { + + @Test + void testDefaultValues() { + var provenance = ObjectProvenance.builder().build(); + assertThat(provenance.getOwnerRef()).isEqualTo(MdoReference.EMPTY); + assertThat(provenance.getModifiedByExtensionRefs()).isEmpty(); + assertThat(provenance.getObjectBelonging()).isEqualTo(ObjectBelonging.OWN); + } + + @Test + void testFilledValues() { + var ownerRef = MdoReference.create("Configuration.МояКонфигурация"); + var extRef = MdoReference.create("Catalog.Расширение1"); + var provenance = ObjectProvenance.builder() + .ownerRef(ownerRef) + .modifiedByExtensionRef(extRef) + .objectBelonging(ObjectBelonging.ADOPTED) + .build(); + assertThat(provenance.getOwnerRef()).isEqualTo(ownerRef); + assertThat(provenance.getModifiedByExtensionRefs()).containsExactly(extRef); + assertThat(provenance.getObjectBelonging()).isEqualTo(ObjectBelonging.ADOPTED); + } + + @Test + void testEquality() { + var ref = MdoReference.create("Configuration.Obj"); + var a = ObjectProvenance.builder().ownerRef(ref).build(); + var b = ObjectProvenance.builder().ownerRef(ref).build(); + assertThat(a).isEqualTo(b); + assertThat(a).hasSameHashCodeAs(b); + } + + @Test + void testToString() { + var provenance = ObjectProvenance.builder().build(); + assertThat(provenance.toString()).contains("ObjectProvenance("); + } +} diff --git a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java new file mode 100644 index 000000000..2402f3489 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java @@ -0,0 +1,108 @@ +/* + * 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 com.github._1c_syntax.bsl.mdo.support.ObjectBelonging; +import com.github._1c_syntax.bsl.types.MdoReference; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class SolutionTest { + + MdoReference baseRef = MdoReference.create("Configuration.МояКонфигурация"); + MdoReference extRef = MdoReference.create("Catalog.Расширение1"); + MdoReference objRef = MdoReference.create("Catalog.Товары"); + + @Test + void testDelegationToEffective() { + var base = Configuration.EMPTY; + var solution = Solution.builder() + .baseConfiguration(base) + .extensions(List.of()) + .mergedConfiguration(base) + .provenance(Map.of()) + .build(); + assertThat(solution.getBaseConfiguration()).isEqualTo(base); + assertThat(solution.isEmpty()).isTrue(); + } + + @Test + void testGetProvenance() { + var base = Configuration.EMPTY; + var prov = ObjectProvenance.builder() + .ownerRef(baseRef) + .objectBelonging(ObjectBelonging.OWN) + .build(); + var solution = Solution.builder() + .baseConfiguration(base) + .mergedConfiguration(base) + .provenance(Map.of(objRef, prov)) + .build(); + assertThat(solution.getProvenance(objRef)).isEqualTo(prov); + assertThat(solution.getOwnerRef(objRef)).isEqualTo(baseRef); + } + + @Test + void testGetProvenanceMissingReturnsNull() { + var solution = Solution.builder() + .mergedConfiguration(Configuration.EMPTY) + .provenance(Map.of()) + .build(); + assertThat(solution.getProvenance(objRef)).isNull(); + } + + @Test + void testGetOwnerReturnsConfiguration() { + var base = Configuration.builder() + .mdoReference(baseRef) + .name("МояКонфигурация") + .build(); + var prov = ObjectProvenance.builder().ownerRef(baseRef).build(); + var solution = Solution.builder() + .baseConfiguration(base) + .mergedConfiguration(base) + .provenance(Map.of(objRef, prov)) + .build(); + assertThat(solution.getOwner(objRef)).isSameAs(base); + } + + @Test + void testGetOwnerReturnsExtension() { + var base = Configuration.EMPTY; + var ext = ConfigurationExtension.builder() + .mdoReference(extRef) + .name("Расширение1") + .build(); + var prov = ObjectProvenance.builder().ownerRef(extRef).build(); + var solution = Solution.builder() + .baseConfiguration(base) + .extensions(List.of(ext)) + .mergedConfiguration(base) + .provenance(Map.of(objRef, prov)) + .build(); + assertThat(solution.getOwner(objRef)).isSameAs(ext); + } +} diff --git a/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java b/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java new file mode 100644 index 000000000..b6ea51744 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java @@ -0,0 +1,211 @@ +/* + * 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.reader; + +import com.github._1c_syntax.bsl.mdclasses.Configuration; +import com.github._1c_syntax.bsl.mdclasses.ConfigurationExtension; +import com.github._1c_syntax.bsl.mdclasses.MDCReadSettings; +import com.github._1c_syntax.bsl.mdclasses.MDClasses; +import com.github._1c_syntax.bsl.test_utils.Fixtures; +import com.github._1c_syntax.bsl.test_utils.assertions.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.provider.CsvSource; + +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class MDMergerMergeTest { + + private static Path basePath(boolean isEDT, String pack) { + return isEDT + ? Path.of("src/test/resources/ext/edt/" + pack + "/configuration") + : Path.of("src/test/resources/ext/designer/" + pack); + } + + @ParameterizedTest + @CsvSource( + { + "false, mdclasses, mdclasses_ext", + "true, mdclasses, mdclasses_ext" + } + ) + void testMergePreservesChildTypes(ArgumentsAccessor argumentsAccessor) { + var isEDT = argumentsAccessor.getBoolean(0); + var basePack = argumentsAccessor.getString(1); + var extPack = argumentsAccessor.getString(2); + + var settings = MDCReadSettings.DEFAULT; + var baseCfgs = MDClasses.createConfigurations(basePath(isEDT, basePack), settings); + var extCfgs = MDClasses.createConfigurations(basePath(isEDT, extPack), settings); + + var base = (Configuration) baseCfgs.stream() + .filter(c -> c instanceof Configuration) + .findFirst().orElseThrow(); + var ext = (ConfigurationExtension) extCfgs.stream() + .filter(c -> c instanceof ConfigurationExtension) + .findFirst().orElseThrow(); + + var merged = MDMerger.merge(base, ext); + + // Расширение изменяет Справочник1 (уже есть в базе) и добавляет Справочник2 + // В результате должно быть 2 справочника: базовый + новый из расширения + assertThat(merged.getCatalogs()).hasSize(2); + + // Проверяем, что дочерние объекты сохранились в объединённом Справочник1 + var catalog1 = merged.getCatalogs().stream() + .filter(c -> "Справочник1".equals(c.getName())) + .findFirst().orElseThrow(); + + // Базовый Справочник1: 3 реквизита, 1 табличная часть, 3 формы, 1 шаблон, 1 команда + // После слияния все типы дочерних объектов должны быть непусты + assertThat(catalog1.getAttributes()).as("Реквизиты должны сохраниться").isNotEmpty(); + assertThat(catalog1.getTabularSections()).as("Табличные части должны сохраниться").isNotEmpty(); + assertThat(catalog1.getForms()).as("Формы должны сохраниться").isNotEmpty(); + assertThat(catalog1.getTemplates()).as("Шаблоны должны сохраниться").isNotEmpty(); + assertThat(catalog1.getCommands()).as("Команды должны сохраниться").isNotEmpty(); + } + + @ParameterizedTest + @CsvSource( + { + "false, mdclasses_ext", + "true, mdclasses_ext" + } + ) + void testMergeWithEmptyBase(ArgumentsAccessor argumentsAccessor) { + var isEDT = argumentsAccessor.getBoolean(0); + var extPack = argumentsAccessor.getString(1); + + var extCfgs = MDClasses.createConfigurations(basePath(isEDT, extPack), MDCReadSettings.DEFAULT); + var ext = (ConfigurationExtension) extCfgs.stream() + .filter(ConfigurationExtension.class::isInstance) + .findFirst().orElseThrow(); + + var merged = MDMerger.merge(Configuration.EMPTY, ext); + + // Расширение содержит 2 справочника: Справочник1 (adopted) и Справочник2 (новый) + assertThat(merged.getCatalogs()).hasSize(2); + assertThat(merged.getCatalogs().getFirst().getAttributes()).isNotEmpty(); + } + + @ParameterizedTest + @CsvSource( + { + "false, mdclasses, mdclasses_ext", + "true, mdclasses, mdclasses_ext" + } + ) + void testMergeAllReturnsProvenance(ArgumentsAccessor argumentsAccessor) { + var isEDT = argumentsAccessor.getBoolean(0); + var basePack = argumentsAccessor.getString(1); + var extPack = argumentsAccessor.getString(2); + + var settings = MDCReadSettings.DEFAULT; + var baseCfgs = MDClasses.createConfigurations(basePath(isEDT, basePack), settings); + var extCfgs = MDClasses.createConfigurations(basePath(isEDT, extPack), settings); + + var base = (Configuration) baseCfgs.stream() + .filter(Configuration.class::isInstance) + .findFirst().orElseThrow(); + var ext = (ConfigurationExtension) extCfgs.stream() + .filter(ConfigurationExtension.class::isInstance) + .findFirst().orElseThrow(); + + var result = MDMerger.mergeAll(base, List.of(ext)); + + assertThat(result.configuration()).isNotNull(); + assertThat(result.provenance()).isNotEmpty(); + + // Проверяем происхождение справочника, который есть и в базе, и в расширении + var catalogRef = result.configuration().getCatalogs().stream() + .filter(c -> "Справочник1".equals(c.getName())) + .findFirst().orElseThrow() + .getMdoReference(); + var prov = result.provenance().get(catalogRef); + assertThat(prov).isNotNull(); + assertThat(prov.getObjectBelonging()).isNotNull(); + } + + @ParameterizedTest + @CsvSource( + { + "false, mdclasses, mdclasses_ext", + "true, mdclasses, mdclasses_ext" + } + ) + void testMergeAllPreservesObjects(ArgumentsAccessor argumentsAccessor) { + var isEDT = argumentsAccessor.getBoolean(0); + var basePack = argumentsAccessor.getString(1); + var extPack = argumentsAccessor.getString(2); + + var settings = MDCReadSettings.DEFAULT; + var baseCfgs = MDClasses.createConfigurations(basePath(isEDT, basePack), settings); + var extCfgs = MDClasses.createConfigurations(basePath(isEDT, extPack), settings); + + var base = (Configuration) baseCfgs.stream() + .filter(Configuration.class::isInstance) + .findFirst().orElseThrow(); + var ext = (ConfigurationExtension) extCfgs.stream() + .filter(ConfigurationExtension.class::isInstance) + .findFirst().orElseThrow(); + + var result = MDMerger.mergeAll(base, List.of(ext)); + + // В базе 1 справочник, расширение добавляет ещё 1 — всего 2 + assertThat(result.configuration().getCatalogs()).hasSize(2); + // Проверяем, что дочерние объекты не потерялись при слиянии + var catalog1 = result.configuration().getCatalogs().stream() + .filter(c -> "Справочник1".equals(c.getName())) + .findFirst().orElseThrow(); + assertThat(catalog1.getAttributes()).isNotEmpty(); + assertThat(catalog1.getForms()).isNotEmpty(); + } + + @Test + void mergeFormatsProduceIdenticalResult() { + var desPath = Path.of("src/test/resources/ext/designer/mdclasses"); + var desExtPath = Path.of("src/test/resources/ext/designer/mdclasses_ext"); + var edtPath = Path.of("src/test/resources/ext/edt/mdclasses/configuration"); + var edtExtPath = Path.of("src/test/resources/ext/edt/mdclasses_ext/configuration"); + + var settings = MDCReadSettings.DEFAULT; + + var desBase = (Configuration) MDClasses.createConfigurations(desPath, settings).stream() + .filter(Configuration.class::isInstance).findFirst().orElseThrow(); + var desExt = (ConfigurationExtension) MDClasses.createConfigurations(desExtPath, settings).stream() + .filter(ConfigurationExtension.class::isInstance).findFirst().orElseThrow(); + var desMerged = MDMerger.merge(desBase, desExt); + + var edtBase = (Configuration) MDClasses.createConfigurations(edtPath, settings).stream() + .filter(Configuration.class::isInstance).findFirst().orElseThrow(); + var edtExt = (ConfigurationExtension) MDClasses.createConfigurations(edtExtPath, settings).stream() + .filter(ConfigurationExtension.class::isInstance).findFirst().orElseThrow(); + var edtMerged = MDMerger.merge(edtBase, edtExt); + + Assertions.assertThat(Fixtures.asJson(desMerged), true) + .isEqual(Fixtures.asJson(edtMerged)); + } +} diff --git a/src/test/java/com/github/_1c_syntax/bsl/smoke/RightTest.java b/src/test/java/com/github/_1c_syntax/bsl/smoke/RightTest.java index b86e5fcf6..04d09ff47 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/smoke/RightTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/smoke/RightTest.java @@ -44,7 +44,7 @@ void possibleRights() { var classes = scanResult.getClassesImplementing(AccessRightsOwner.class); - assertThat(classes).hasSize(48); + assertThat(classes).hasSize(49); var list = classes.stream() .filter(ci -> !ci.isInterface()) From a7557903954ea76245f70c4935a731bcbe2047aa Mon Sep 17 00:00:00 2001 From: theshadowco Date: Wed, 8 Jul 2026 09:15:33 +0300 Subject: [PATCH 2/4] sq fixes --- .../_1c_syntax/bsl/mdclasses/Solution.java | 47 +++++++++++++++++-- .../_1c_syntax/bsl/reader/MDMerger.java | 12 ++++- .../bsl/mdclasses/SolutionTest.java | 10 ++++ .../bsl/reader/MDMergerMergeTest.java | 21 +++++++++ 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/_1c_syntax/bsl/mdclasses/Solution.java b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/Solution.java index b6de7d357..7ec09918e 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/mdclasses/Solution.java +++ b/src/main/java/com/github/_1c_syntax/bsl/mdclasses/Solution.java @@ -48,7 +48,7 @@ public class Solution implements CF { * Пустое решение (нет объектов метаданных, нет расширений) */ public static final Solution EMPTY = new Solution( - Configuration.EMPTY, Configuration.EMPTY, Collections.emptyList(), Map.of()); + Configuration.EMPTY, Configuration.EMPTY, Collections.emptyList(), Collections.emptyMap()); /** * Объединенная конфигурация — результат слияния основной конфигурации со всеми расширениями @@ -72,7 +72,8 @@ public class Solution implements CF { * Хранит, какой конфигурации или расширению принадлежит объект и какими расширениями * был изменен. */ - Map provenance; + @Builder.Default + Map provenance = Collections.emptyMap(); /** * Возвращает принадлежность для заданной ссылки на объект метаданных. @@ -110,15 +111,53 @@ public MdoReference getOwnerRef(MdoReference mdoRef) { @Nullable public MDClass getOwner(MdoReference mdoRef) { var ownerRef = getOwnerRef(mdoRef); - if (ownerRef.equals(baseConfiguration.getMdoReference())) { + return getOwnerByRef(ownerRef); + } + + /** + * Возвращает конфигурацию или расширение ({@link CF}) по ссылке на неё. + *

+ * Позволяет получить экземпляр {@link Configuration} или {@link ConfigurationExtension} + * из {@code ownerRef}, сохранённого в {@link ObjectProvenance#getOwnerRef()}. + * + * @param cfMdoRef ссылка на конфигурацию или расширение + * @return конфигурация или расширение, или {@code null}, если не найдено + */ + @Nullable + public CF getOwnerByRef(MdoReference cfMdoRef) { + if (cfMdoRef.equals(baseConfiguration.getMdoReference())) { return baseConfiguration; } return extensions.stream() - .filter(ext -> ext.getMdoReference().equals(ownerRef)) + .filter(ext -> ext.getMdoReference().equals(cfMdoRef)) .findFirst() .orElse(null); } + /** + * Проверяет, входит ли конфигурация или расширение в состав решения. + * + * @param cf конфигурация или расширение + * @return {@code true}, если {@code cf} является базовой конфигурацией + * или одним из расширений + */ + public boolean contains(CF cf) { + return cf == baseConfiguration + || (cf instanceof ConfigurationExtension ext && extensions.contains(ext)); + } + + /** + * Проверяет, входит ли конфигурация или расширение в состав решения + * по ссылке на неё. + * + * @param ref ссылка на конфигурацию или расширение + * @return {@code true}, если {@code ref} соответствует базовой конфигурации + * или одному из расширений + */ + public boolean contains(MdoReference ref) { + return getOwnerByRef(ref) != null; + } + /** * Возвращает перечень возможных прав доступа */ diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java b/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java index b4b57c935..db0c0d934 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java @@ -468,7 +468,17 @@ private static Object copyCommandOwner(CommandOwner srcMD, private static void initProvenance(CF cf, MdoReference ownerRef, Map provenance) { cf.getChildrenByMdoRef().forEach((MdoReference ref, MD md) -> { - if (!provenance.containsKey(ref)) { + var existing = provenance.get(ref); + if (existing != null) { + // Объект уже есть от базы (или предыдущего расширения) — + // текущий cf расширяет его, добавляем ownerRef в список модифицировавших + provenance.put(ref, ObjectProvenance.builder() + .ownerRef(existing.getOwnerRef()) + .objectBelonging(md.getObjectBelonging()) + .modifiedByExtensionRefs(existing.getModifiedByExtensionRefs()) + .modifiedByExtensionRef(ownerRef) + .build()); + } else { provenance.put(ref, ObjectProvenance.builder() .ownerRef(ownerRef) .objectBelonging(md.getObjectBelonging()) diff --git a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java index 2402f3489..9d2acf3c6 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java @@ -89,6 +89,16 @@ void testGetOwnerReturnsConfiguration() { assertThat(solution.getOwner(objRef)).isSameAs(base); } + @Test + void testNullSafeWhenProvenanceNotSet() { + var solution = Solution.builder() + .mergedConfiguration(Configuration.EMPTY) + .build(); + assertThat(solution.getProvenance(objRef)).isNull(); + assertThat(solution.getOwnerRef(objRef)).isEqualTo(MdoReference.EMPTY); + assertThat(solution.getOwner(objRef)).isEqualTo(Configuration.EMPTY); + } + @Test void testGetOwnerReturnsExtension() { var base = Configuration.EMPTY; diff --git a/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java b/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java index b6ea51744..e42bc1ded 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java @@ -146,7 +146,28 @@ void testMergeAllReturnsProvenance(ArgumentsAccessor argumentsAccessor) { .getMdoReference(); var prov = result.provenance().get(catalogRef); assertThat(prov).isNotNull(); + assertThat(prov.getOwnerRef()) + .as("Объект из базы должен иметь ownerRef = базовая конфигурация") + .isEqualTo(base.getMdoReference()); assertThat(prov.getObjectBelonging()).isNotNull(); + // Расширение модифицирует Справочник1 — проверяем, что ref расширения попал в modifiedByExtensionRefs + assertThat(prov.getModifiedByExtensionRefs()) + .as("Расширение, модифицирующее объект, должно быть в modifiedByExtensionRefs") + .contains(ext.getMdoReference()); + + // Проверяем происхождение справочника, которого нет в базе (только в расширении) + var catalog2Ref = result.configuration().getCatalogs().stream() + .filter(c -> "Справочник2".equals(c.getName())) + .findFirst().orElseThrow() + .getMdoReference(); + var prov2 = result.provenance().get(catalog2Ref); + assertThat(prov2).isNotNull(); + assertThat(prov2.getOwnerRef()) + .as("Новый объект из расширения должен иметь ownerRef = расширение") + .isEqualTo(ext.getMdoReference()); + assertThat(prov2.getModifiedByExtensionRefs()) + .as("Новый объект не модифицировался расширениями") + .isEmpty(); } @ParameterizedTest From c33227d8020661f1d225479283c9ef5cc039437a Mon Sep 17 00:00:00 2001 From: theshadowco Date: Wed, 8 Jul 2026 09:15:50 +0300 Subject: [PATCH 3/4] tests --- .../bsl/mdclasses/SolutionTest.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java index 9d2acf3c6..09d32406b 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/SolutionTest.java @@ -115,4 +115,87 @@ void testGetOwnerReturnsExtension() { .build(); assertThat(solution.getOwner(objRef)).isSameAs(ext); } + + @Test + void testGetOwnerByRefReturnsConfiguration() { + var base = Configuration.builder() + .mdoReference(baseRef) + .name("МояКонфигурация") + .build(); + var solution = Solution.builder() + .baseConfiguration(base) + .mergedConfiguration(base) + .provenance(Map.of()) + .build(); + assertThat(solution.getOwnerByRef(baseRef)).isSameAs(base); + } + + @Test + void testGetOwnerByRefReturnsExtension() { + var ext = ConfigurationExtension.builder() + .mdoReference(extRef) + .name("Расширение1") + .build(); + var solution = Solution.builder() + .baseConfiguration(Configuration.EMPTY) + .extensions(List.of(ext)) + .mergedConfiguration(Configuration.EMPTY) + .provenance(Map.of()) + .build(); + assertThat(solution.getOwnerByRef(extRef)).isSameAs(ext); + } + + @Test + void testGetOwnerByRefReturnsNullForUnknownRef() { + var unknownRef = MdoReference.create("Configuration.ЧужаяКонфигурация"); + var solution = Solution.builder() + .mergedConfiguration(Configuration.EMPTY) + .provenance(Map.of()) + .build(); + assertThat(solution.getOwnerByRef(unknownRef)).isNull(); + } + + @Test + void testContainsByObject() { + var base = Configuration.builder() + .mdoReference(baseRef) + .name("МояКонфигурация") + .build(); + var ext = ConfigurationExtension.builder() + .mdoReference(extRef) + .name("Расширение1") + .build(); + var solution = Solution.builder() + .baseConfiguration(base) + .extensions(List.of(ext)) + .mergedConfiguration(base) + .provenance(Map.of()) + .build(); + + assertThat(solution.contains(base)).isTrue(); + assertThat(solution.contains(ext)).isTrue(); + assertThat(solution.contains(Configuration.EMPTY)).isFalse(); + } + + @Test + void testContainsByReference() { + var base = Configuration.builder() + .mdoReference(baseRef) + .name("МояКонфигурация") + .build(); + var ext = ConfigurationExtension.builder() + .mdoReference(extRef) + .name("Расширение1") + .build(); + var solution = Solution.builder() + .baseConfiguration(base) + .extensions(List.of(ext)) + .mergedConfiguration(base) + .provenance(Map.of()) + .build(); + + assertThat(solution.contains(baseRef)).isTrue(); + assertThat(solution.contains(extRef)).isTrue(); + assertThat(solution.contains(MdoReference.create("Configuration.ЧужаяКонфигурация"))).isFalse(); + } } From f80b02355eec65f90c0396cc8b7b97598d2f64dd Mon Sep 17 00:00:00 2001 From: theshadowco Date: Wed, 8 Jul 2026 09:26:28 +0300 Subject: [PATCH 4/4] sq fixes --- .../_1c_syntax/bsl/mdclasses/MDClasses.java | 109 ++++----- .../_1c_syntax/bsl/reader/MDMerger.java | 216 +++++++++--------- .../edt/converter/ConfigurationConverter.java | 2 +- .../bsl/mdclasses/ObjectProvenanceTest.java | 3 +- .../bsl/reader/MDMergerMergeTest.java | 4 +- 5 files changed, 169 insertions(+), 165 deletions(-) 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 f2f09b417..fe03b299d 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 @@ -140,40 +140,39 @@ public Solution createSolution(Path sourcePath) { public Solution createSolution(Path sourcePath, MDCReadSettings readSettings) { var mdcs = createConfigurations(sourcePath, readSettings); - if (mdcs.isEmpty()) { - return Solution.EMPTY; - } + if (!mdcs.isEmpty()) { + if (mdcs.size() == 1) { + var mdc = mdcs.getFirst(); + if (mdc instanceof Configuration cf) { + return buildSolution(cf, Collections.emptyList()); + } + if (mdc instanceof ConfigurationExtension ext) { + return buildSolution(Configuration.EMPTY, List.of(ext)); + } - if (mdcs.size() == 1) { - var mdc = mdcs.getFirst(); - if (mdc instanceof Configuration cf) { - return buildSolution(cf, Collections.emptyList()); - } - if (mdc instanceof ConfigurationExtension ext) { - return buildSolution(Configuration.EMPTY, List.of(ext)); + // заглушка, если получилось что-то странное + return Solution.EMPTY; } - // заглушка, если получилось что-то странное - return Solution.EMPTY; + // решение содержит несколько компонент, разделяем + var base = mdcs.stream() + .filter(Configuration.class::isInstance) + .map(Configuration.class::cast) + .findFirst() + .orElse(Configuration.EMPTY); + + var extensions = mdcs.stream() + .filter(ConfigurationExtension.class::isInstance) + .map(ConfigurationExtension.class::cast) + .toList(); + + // что-то странное прочли + if (!base.isEmpty() || !extensions.isEmpty()) { + return buildSolution(base, extensions); + } } - // решение содержит несколько компонент, разделяем - var base = mdcs.stream() - .filter(Configuration.class::isInstance) - .map(Configuration.class::cast) - .findFirst() - .orElse(Configuration.EMPTY); - - var extensions = mdcs.stream() - .filter(ConfigurationExtension.class::isInstance) - .map(ConfigurationExtension.class::cast) - .toList(); - - // что-то странное прочли - if (base.isEmpty() && extensions.isEmpty()) { - return Solution.EMPTY; - } - return buildSolution(base, extensions); + return Solution.EMPTY; } /** @@ -253,29 +252,7 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) { @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) { - if (!attrs.isRegularFile()) { - return FileVisitResult.CONTINUE; - } - - var parentName = path.getParent().getFileName().toString(); - var parentParentName = ""; - if (path.getParent().getParent() != null && path.getParent().getParent().getFileName() != null) { - parentParentName = path.getParent().getParent().getFileName().toString(); - } - - if (excludeFolders.contains(parentName) || excludeFolders.contains(parentParentName)) { - return FileVisitResult.CONTINUE; - } - var fileName = path.getFileName().toString(); - var ext = FilenameUtils.getExtension(fileName); - if (!("xml".equals(ext) || "mdo".equals(ext))) { - return FileVisitResult.CONTINUE; - } - - if (pattern.matcher(fileName).matches()) { - listPath.add(path); - } - return FileVisitResult.CONTINUE; + return handleFile(path, attrs, excludeFolders, pattern, listPath); } }); } catch (IOException e) { @@ -285,6 +262,34 @@ public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) { return listPath; } + private static FileVisitResult handleFile(Path path, BasicFileAttributes attrs, + Set excludeFolders, Pattern pattern, + List listPath) { + if (!attrs.isRegularFile()) { + return FileVisitResult.CONTINUE; + } + + var parentName = path.getParent().getFileName().toString(); + var parentParentName = ""; + if (path.getParent().getParent() != null && path.getParent().getParent().getFileName() != null) { + parentParentName = path.getParent().getParent().getFileName().toString(); + } + + if (excludeFolders.contains(parentName) || excludeFolders.contains(parentParentName)) { + return FileVisitResult.CONTINUE; + } + var fileName = path.getFileName().toString(); + var ext = FilenameUtils.getExtension(fileName); + if (!("xml".equals(ext) || "mdo".equals(ext))) { + return FileVisitResult.CONTINUE; + } + + if (pattern.matcher(fileName).matches()) { + listPath.add(path); + } + return FileVisitResult.CONTINUE; + } + private Set mdoTypeGroupNames() { return Arrays.stream(MDOType.values()) .filter(type -> type != MDOType.EXTERNAL_REPORT diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java b/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java index db0c0d934..29a1e62d2 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/MDMerger.java @@ -140,11 +140,20 @@ public static Configuration merge(Configuration cf, ConfigurationExtension exten .clearChartsOfCharacteristicTypes() .chartsOfCharacteristicTypes(mergeMD(cf, extension, CF::getChartsOfCharacteristicTypes, newChildren)) .clearChartsOfAccounts().chartsOfAccounts(mergeMD(cf, extension, CF::getChartsOfAccounts, newChildren)) - .clearChartsOfCalculationTypes().chartsOfCalculationTypes(mergeMD(cf, extension, CF::getChartsOfCalculationTypes, newChildren)) - .clearInformationRegisters().informationRegisters(mergeMD(cf, extension, CF::getInformationRegisters, newChildren)) - .clearAccumulationRegisters().accumulationRegisters(mergeMD(cf, extension, CF::getAccumulationRegisters, newChildren)) + + .clearChartsOfCalculationTypes() + .chartsOfCalculationTypes(mergeMD(cf, extension, CF::getChartsOfCalculationTypes, newChildren)) + + .clearInformationRegisters() + .informationRegisters(mergeMD(cf, extension, CF::getInformationRegisters, newChildren)) + .clearAccumulationRegisters() + + .accumulationRegisters(mergeMD(cf, extension, CF::getAccumulationRegisters, newChildren)) .clearAccountingRegisters().accountingRegisters(mergeMD(cf, extension, CF::getAccountingRegisters, newChildren)) - .clearCalculationRegisters().calculationRegisters(mergeMD(cf, extension, CF::getCalculationRegisters, newChildren)) + + .clearCalculationRegisters() + .calculationRegisters(mergeMD(cf, extension, CF::getCalculationRegisters, newChildren)) + .clearBusinessProcesses().businessProcesses(mergeMD(cf, extension, CF::getBusinessProcesses, newChildren)) .clearTasks().tasks(mergeMD(cf, extension, CF::getTasks, newChildren)) .clearExternalDataSources().externalDataSources(mergeMD(cf, extension, CF::getExternalDataSources, newChildren)) @@ -298,114 +307,13 @@ private static T copyModuleOwner(T srcMD, T modMD) { private static T copyChildrenOwner(T srcMD, T modMD) { var result = srcMD; if (result instanceof ChildrenOwner childrenOwner) { - Object builder = null; - var newChildren = new ArrayList<>( childrenOwner.getChildren().stream() - .filter( - child -> !(child instanceof Form) - && !(child instanceof Template) - && !(child instanceof Command) - && !(child instanceof Attribute) - && !(child instanceof TabularSection) - && !(child instanceof PredefinedValue) - && !(child instanceof EnumValue) - && !(child instanceof Recalculation) - && !(child instanceof TaskAddressingAttribute) - && !(child instanceof ExternalDataSourceTable) - && !(child instanceof ExternalDataSourceCube) - && !(child instanceof ExternalDataSourceFunction) - && !(child instanceof ExternalDataSourceCubeDimensionTable) - ).toList() + .filter(child -> !isSpecialChild(child)) + .toList() ); - if (srcMD instanceof FormOwner formOwner) { - builder = copyFormOwner(formOwner, (FormOwner) modMD, builder, newChildren); - } - - if (srcMD instanceof TemplateOwner templateOwner) { - builder = copyTemplateOwner(templateOwner, (TemplateOwner) modMD, builder, newChildren); - } - - if (srcMD instanceof CommandOwner commandOwner) { - builder = copyCommandOwner(commandOwner, (CommandOwner) modMD, builder, newChildren); - } - - // ReferenceObject - attributes (Catalog, Document, ChartOfAccounts, Task, etc.) - if (srcMD instanceof ReferenceObject referenceObject) { - builder = copyChildrenList(referenceObject, (ReferenceObject) modMD, builder, - ReferenceObject::getAttributes, "attributes", newChildren); - } - - // TabularSectionOwner - tabular sections - if (srcMD instanceof TabularSectionOwner tabularSectionOwner) { - builder = copyChildrenList(tabularSectionOwner, (TabularSectionOwner) modMD, builder, - TabularSectionOwner::getTabularSections, "tabularSections", newChildren); - } - - // PredefinedDataOwner - predefined values - if (srcMD instanceof PredefinedDataOwner predefinedDataOwner) { - builder = copyChildrenList(predefinedDataOwner, (PredefinedDataOwner) modMD, builder, - PredefinedDataOwner::getPredefinedValues, "predefinedValues", newChildren); - } - - // Register - attributes, resources, dimensions - if (srcMD instanceof Register register) { - builder = copyChildrenList(register, (Register) modMD, builder, - Register::getAttributes, "attributes", newChildren); - builder = copyChildrenList(register, (Register) modMD, builder, - Register::getResources, "resources", newChildren); - builder = copyChildrenList(register, (Register) modMD, builder, - Register::getDimensions, "dimensions", newChildren); - } - - // Enum - attributes, enumValues - if (srcMD instanceof Enum anEnum) { - builder = copyChildrenList(anEnum, (Enum) modMD, builder, - Enum::getAttributes, "attributes", newChildren); - builder = copyChildrenList(anEnum, (Enum) modMD, builder, - Enum::getEnumValues, "enumValues", newChildren); - } - - // Task - addressing attributes - if (srcMD instanceof Task task) { - builder = copyChildrenList(task, (Task) modMD, builder, - Task::getAddressingAttributes, "addressingAttributes", newChildren); - } - - // CalculationRegister - recalculations - if (srcMD instanceof CalculationRegister calculationRegister) { - builder = copyChildrenList(calculationRegister, (CalculationRegister) modMD, builder, - CalculationRegister::getRecalculations, "recalculations", newChildren); - } - - // ExternalDataSource - tables, cubes, functions - if (srcMD instanceof ExternalDataSource externalDataSource) { - builder = copyChildrenList(externalDataSource, (ExternalDataSource) modMD, builder, - ExternalDataSource::getTables, "tables", newChildren); - builder = copyChildrenList(externalDataSource, (ExternalDataSource) modMD, builder, - ExternalDataSource::getCubes, "cubes", newChildren); - builder = copyChildrenList(externalDataSource, (ExternalDataSource) modMD, builder, - ExternalDataSource::getFunctions, "functions", newChildren); - } - - // ExternalDataSourceCube - dimensionTables, dimensions, resources - if (srcMD instanceof ExternalDataSourceCube externalDataSourceCube) { - builder = copyChildrenList(externalDataSourceCube, (ExternalDataSourceCube) modMD, builder, - ExternalDataSourceCube::getDimensionTables, "dimensionTables", newChildren); - builder = copyChildrenList(externalDataSourceCube, (ExternalDataSourceCube) modMD, builder, - ExternalDataSourceCube::getDimensions, "dimensions", newChildren); - builder = copyChildrenList(externalDataSourceCube, (ExternalDataSourceCube) modMD, builder, - ExternalDataSourceCube::getResources, "resources", newChildren); - } - - // ChartOfAccounts - accountingFlags, extDimensionAccountingFlags - if (srcMD instanceof ChartOfAccounts chartOfAccounts) { - builder = copyChildrenList(chartOfAccounts, (ChartOfAccounts) modMD, builder, - ChartOfAccounts::getAccountingFlags, "accountingFlags", newChildren); - builder = copyChildrenList(chartOfAccounts, (ChartOfAccounts) modMD, builder, - ChartOfAccounts::getExtDimensionAccountingFlags, "extDimensionAccountingFlags", newChildren); - } + var builder = copyChildrenByType(srcMD, modMD, null, newChildren); if (builder != null) { TransformationUtils.invoke(builder, "clearChildren"); @@ -418,6 +326,98 @@ private static T copyChildrenOwner(T srcMD, T modMD) { return result; } + /** + * Проверяет, является ли дочерний объект «специальным» (обрабатывается отдельно, + * а не через общий список children). + */ + private static boolean isSpecialChild(MD child) { + return child instanceof Form + || child instanceof Template + || child instanceof Command + || child instanceof Attribute + || child instanceof TabularSection + || child instanceof PredefinedValue + || child instanceof EnumValue + || child instanceof Recalculation + || child instanceof TaskAddressingAttribute + || child instanceof ExternalDataSourceTable + || child instanceof ExternalDataSourceCube + || child instanceof ExternalDataSourceFunction + || child instanceof ExternalDataSourceCubeDimensionTable; + } + + @Nullable + private static Object copyChildrenByType(T srcMD, T modMD, + @Nullable Object builder, + ArrayList newChildren) { + if (srcMD instanceof FormOwner formOwner) { + builder = copyFormOwner(formOwner, (FormOwner) modMD, builder, newChildren); + } + if (srcMD instanceof TemplateOwner templateOwner) { + builder = copyTemplateOwner(templateOwner, (TemplateOwner) modMD, builder, newChildren); + } + if (srcMD instanceof CommandOwner commandOwner) { + builder = copyCommandOwner(commandOwner, (CommandOwner) modMD, builder, newChildren); + } + if (srcMD instanceof ReferenceObject referenceObject) { + builder = copyChildrenList(referenceObject, (ReferenceObject) modMD, builder, + ReferenceObject::getAttributes, "attributes", newChildren); + } + if (srcMD instanceof TabularSectionOwner tabularSectionOwner) { + builder = copyChildrenList(tabularSectionOwner, (TabularSectionOwner) modMD, builder, + TabularSectionOwner::getTabularSections, "tabularSections", newChildren); + } + if (srcMD instanceof PredefinedDataOwner predefinedDataOwner) { + builder = copyChildrenList(predefinedDataOwner, (PredefinedDataOwner) modMD, builder, + PredefinedDataOwner::getPredefinedValues, "predefinedValues", newChildren); + } + if (srcMD instanceof Register register) { + builder = copyChildrenList(register, (Register) modMD, builder, + Register::getAttributes, "attributes", newChildren); + builder = copyChildrenList(register, (Register) modMD, builder, + Register::getResources, "resources", newChildren); + builder = copyChildrenList(register, (Register) modMD, builder, + Register::getDimensions, "dimensions", newChildren); + } + if (srcMD instanceof Enum anEnum) { + builder = copyChildrenList(anEnum, (Enum) modMD, builder, + Enum::getAttributes, "attributes", newChildren); + builder = copyChildrenList(anEnum, (Enum) modMD, builder, + Enum::getEnumValues, "enumValues", newChildren); + } + if (srcMD instanceof Task task) { + builder = copyChildrenList(task, (Task) modMD, builder, + Task::getAddressingAttributes, "addressingAttributes", newChildren); + } + if (srcMD instanceof CalculationRegister calculationRegister) { + builder = copyChildrenList(calculationRegister, (CalculationRegister) modMD, builder, + CalculationRegister::getRecalculations, "recalculations", newChildren); + } + if (srcMD instanceof ExternalDataSource externalDataSource) { + builder = copyChildrenList(externalDataSource, (ExternalDataSource) modMD, builder, + ExternalDataSource::getTables, "tables", newChildren); + builder = copyChildrenList(externalDataSource, (ExternalDataSource) modMD, builder, + ExternalDataSource::getCubes, "cubes", newChildren); + builder = copyChildrenList(externalDataSource, (ExternalDataSource) modMD, builder, + ExternalDataSource::getFunctions, "functions", newChildren); + } + if (srcMD instanceof ExternalDataSourceCube externalDataSourceCube) { + builder = copyChildrenList(externalDataSourceCube, (ExternalDataSourceCube) modMD, builder, + ExternalDataSourceCube::getDimensionTables, "dimensionTables", newChildren); + builder = copyChildrenList(externalDataSourceCube, (ExternalDataSourceCube) modMD, builder, + ExternalDataSourceCube::getDimensions, "dimensions", newChildren); + builder = copyChildrenList(externalDataSourceCube, (ExternalDataSourceCube) modMD, builder, + ExternalDataSourceCube::getResources, "resources", newChildren); + } + if (srcMD instanceof ChartOfAccounts chartOfAccounts) { + builder = copyChildrenList(chartOfAccounts, (ChartOfAccounts) modMD, builder, + ChartOfAccounts::getAccountingFlags, "accountingFlags", newChildren); + builder = copyChildrenList(chartOfAccounts, (ChartOfAccounts) modMD, builder, + ChartOfAccounts::getExtDimensionAccountingFlags, "extDimensionAccountingFlags", newChildren); + } + return builder; + } + @Nullable private static Object copyChildrenList(K srcMD, K modMD, diff --git a/src/main/java/com/github/_1c_syntax/bsl/reader/edt/converter/ConfigurationConverter.java b/src/main/java/com/github/_1c_syntax/bsl/reader/edt/converter/ConfigurationConverter.java index ed59e6180..da26b1370 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/reader/edt/converter/ConfigurationConverter.java +++ b/src/main/java/com/github/_1c_syntax/bsl/reader/edt/converter/ConfigurationConverter.java @@ -74,7 +74,7 @@ private static boolean hasExtensionMarker(HierarchicalStreamReader reader) { // Проверка через namespace (некоторые форматы EDT не выставляют xsi:type на корне) if (reader instanceof ExtendReaderWrapper wrapper) { var xmlReader = wrapper.getXMLStreamReader(); - for (int i = 0; i < xmlReader.getNamespaceCount(); i++) { + for (var i = 0; i < xmlReader.getNamespaceCount(); i++) { if ("mdclassExtension".equals(xmlReader.getNamespacePrefix(i))) { return true; } diff --git a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenanceTest.java b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenanceTest.java index b82a3b4bb..aafffe8b4 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenanceTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/mdclasses/ObjectProvenanceTest.java @@ -56,8 +56,7 @@ void testEquality() { var ref = MdoReference.create("Configuration.Obj"); var a = ObjectProvenance.builder().ownerRef(ref).build(); var b = ObjectProvenance.builder().ownerRef(ref).build(); - assertThat(a).isEqualTo(b); - assertThat(a).hasSameHashCodeAs(b); + assertThat(a).isEqualTo(b).hasSameHashCodeAs(b); } @Test diff --git a/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java b/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java index e42bc1ded..2e0fc235f 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/reader/MDMergerMergeTest.java @@ -62,10 +62,10 @@ void testMergePreservesChildTypes(ArgumentsAccessor argumentsAccessor) { var extCfgs = MDClasses.createConfigurations(basePath(isEDT, extPack), settings); var base = (Configuration) baseCfgs.stream() - .filter(c -> c instanceof Configuration) + .filter(Configuration.class::isInstance) .findFirst().orElseThrow(); var ext = (ConfigurationExtension) extCfgs.stream() - .filter(c -> c instanceof ConfigurationExtension) + .filter(ConfigurationExtension.class::isInstance) .findFirst().orElseThrow(); var merged = MDMerger.merge(base, ext);