1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.testsupport.builder;
7   
8   import static org.mockito.ArgumentMatchers.eq;
9   import static org.mockito.Mockito.doReturn;
10  
11  import java.util.ArrayList;
12  import java.util.List;
13  import java.util.Map;
14  import java.util.function.Function;
15  import java.util.stream.Collectors;
16  
17  import dev.metaschema.core.model.IAssemblyDefinition;
18  import dev.metaschema.core.model.IAssemblyInstance;
19  import dev.metaschema.core.model.IAssemblyInstanceAbsolute;
20  import dev.metaschema.core.model.IFieldDefinition;
21  import dev.metaschema.core.model.IFieldInstance;
22  import dev.metaschema.core.model.IFlagInstance;
23  import dev.metaschema.core.model.IModule;
24  import dev.metaschema.core.model.INamedModelElement;
25  import dev.metaschema.core.model.INamedModelInstanceAbsolute;
26  import dev.metaschema.core.model.ISource;
27  import dev.metaschema.core.model.ModelType;
28  import dev.metaschema.core.model.constraint.AssemblyConstraintSet;
29  import dev.metaschema.core.qname.IEnhancedQName;
30  import dev.metaschema.core.util.CollectionUtil;
31  import dev.metaschema.core.util.ObjectUtils;
32  import edu.umd.cs.findbugs.annotations.NonNull;
33  import edu.umd.cs.findbugs.annotations.Nullable;
34  
35  final class AssemblyBuilder
36      extends AbstractModelBuilder<IAssemblyBuilder>
37      implements IAssemblyBuilder {
38  
39    private String rootNamespace = "";
40    private String rootName;
41  
42    private List<? extends IModelBuilder<?>> modelInstances;
43  
44    AssemblyBuilder() {
45      // prevent direct instantiation
46    }
47  
48    @Override
49    public AssemblyBuilder reset() {
50      super.reset();
51      this.modelInstances = CollectionUtil.emptyList();
52      return this;
53    }
54  
55    @Override
56    @NonNull
57    public AssemblyBuilder rootNamespace(@NonNull String name) {
58      this.rootNamespace = name;
59      return this;
60    }
61  
62    @Override
63    @NonNull
64    public AssemblyBuilder rootName(@NonNull String name) {
65      this.rootName = name;
66      return this;
67    }
68  
69    @Override
70    @NonNull
71    public AssemblyBuilder rootQName(@NonNull IEnhancedQName qname) {
72      this.rootName = qname.getLocalName();
73      this.rootNamespace = qname.getNamespace();
74      return this;
75    }
76  
77    @Override
78    public AssemblyBuilder modelInstances(@Nullable List<? extends IModelBuilder<?>> modelInstances) {
79      this.modelInstances = modelInstances == null ? CollectionUtil.emptyList() : modelInstances;
80      return this;
81    }
82  
83    /**
84     * Get the model instance builders configured for this assembly.
85     *
86     * @return the list of model instance builders
87     */
88    @NonNull
89    List<? extends IModelBuilder<?>> getModelInstanceBuilders() {
90      return this.modelInstances;
91    }
92  
93    /**
94     * Check if any model instances are references that need lazy resolution.
95     *
96     * @return {@code true} if any model instance is an {@link IModelReference}
97     */
98    boolean hasModelReferences() {
99      return modelInstances.stream().anyMatch(IModelReference.class::isInstance);
100   }
101 
102   @Override
103   @NonNull
104   public IAssemblyInstanceAbsolute toInstance(@NonNull IAssemblyDefinition parent) {
105     IAssemblyDefinition def = toDefinition();
106     return toInstance(parent, def);
107   }
108 
109   /**
110    * Build a mocked assembly instance, using the provided definition, as a child
111    * of the provided parent.
112    *
113    * @param parent
114    *          the parent containing the new instance
115    * @param definition
116    *          the definition to base the instance on
117    * @return the new mocked instance
118    */
119   @Override
120   @NonNull
121   public IAssemblyInstanceAbsolute toInstance(
122       @NonNull IAssemblyDefinition parent,
123       @NonNull IAssemblyDefinition definition) {
124     validate();
125 
126     IAssemblyInstanceAbsolute retval = mock(IAssemblyInstanceAbsolute.class);
127     applyNamedInstance(retval, definition, parent);
128     return retval;
129   }
130 
131   /**
132    * Build a mocked assembly definition.
133    *
134    * @return the new mocked definition
135    */
136   @Override
137   @NonNull
138   public IAssemblyDefinition toDefinition() {
139     return toDefinition(null);
140   }
141 
142   @Override
143   @NonNull
144   public IAssemblyDefinition toDefinition(@Nullable IModule module) {
145     validate();
146 
147     // already validated as non-null
148     ISource source = ObjectUtils.notNull(getSource());
149 
150     IAssemblyDefinition retval = mock(IAssemblyDefinition.class);
151     applyDefinition(retval, module);
152 
153     Map<IEnhancedQName, IFlagInstance> flags = getFlags().stream()
154         .map(builder -> builder.source(source).toInstance(retval))
155         .collect(Collectors.toUnmodifiableMap(
156             IFlagInstance::getQName,
157             Function.identity()));
158 
159     if (rootName != null) {
160       doReturn(ModelType.ASSEMBLY).when(retval).getModelType();
161 
162       IEnhancedQName rootQName = IEnhancedQName.of(ObjectUtils.notNull(rootNamespace), ObjectUtils.notNull(rootName));
163       doReturn(rootQName).when(retval).getRootQName();
164     }
165 
166     doReturn(new AssemblyConstraintSet(source)).when(retval).getConstraintSupport();
167 
168     doReturn(flags.values()).when(retval).getFlagInstances();
169     flags.entrySet().forEach(entry -> {
170       assert entry != null;
171       doReturn(entry.getValue()).when(retval).getFlagInstanceByName(eq(entry.getKey().getIndexPosition()));
172     });
173 
174     Map<IEnhancedQName, ? extends INamedModelInstanceAbsolute> modelInstances = this.modelInstances.stream()
175         .map(builder -> builder.source(source).toInstance(retval))
176         .collect(Collectors.toUnmodifiableMap(
177             INamedModelInstanceAbsolute::getQName,
178             Function.identity()));
179 
180     doReturn(modelInstances.values()).when(retval).getModelInstances();
181     doReturn(CollectionUtil.emptyMap()).when(retval).getChoiceGroupInstances();
182     doReturn(CollectionUtil.emptyList()).when(retval).getChoiceInstances();
183     modelInstances.forEach((key, value) -> {
184       doReturn(value).when(retval).getNamedModelInstanceByName(eq(key.getIndexPosition()));
185 
186       if (value instanceof IAssemblyInstance) {
187         doReturn(value).when(retval).getAssemblyInstanceByName(eq(key.getIndexPosition()));
188       } else if (value instanceof IFieldInstance) {
189         doReturn(value).when(retval).getFieldInstanceByName(eq(key.getIndexPosition()));
190       }
191     });
192     doReturn(
193         modelInstances.values().stream()
194             .filter(IAssemblyInstance.class::isInstance)
195             .collect(Collectors.toList()))
196                 .when(retval).getAssemblyInstances();
197     doReturn(
198         modelInstances.values().stream()
199             .filter(IFieldInstance.class::isInstance)
200             .collect(Collectors.toList()))
201                 .when(retval).getFieldInstances();
202     return retval;
203   }
204 
205   /**
206    * Build a mocked assembly definition without model instances. This is used for
207    * two-phase construction when references need to be resolved later.
208    *
209    * @param module
210    *          the containing module
211    * @return the new mocked definition with empty model instances
212    */
213   @NonNull
214   IAssemblyDefinition toDefinitionShell(@Nullable IModule module) {
215     validate();
216 
217     // already validated as non-null
218     ISource source = ObjectUtils.notNull(getSource());
219 
220     IAssemblyDefinition retval = mock(IAssemblyDefinition.class);
221     applyDefinition(retval, module);
222 
223     Map<IEnhancedQName, IFlagInstance> flags = getFlags().stream()
224         .map(builder -> builder.source(source).toInstance(retval))
225         .collect(Collectors.toUnmodifiableMap(
226             IFlagInstance::getQName,
227             Function.identity()));
228 
229     if (rootName != null) {
230       doReturn(ModelType.ASSEMBLY).when(retval).getModelType();
231 
232       IEnhancedQName rootQName = IEnhancedQName.of(ObjectUtils.notNull(rootNamespace), ObjectUtils.notNull(rootName));
233       doReturn(rootQName).when(retval).getRootQName();
234     }
235 
236     doReturn(new AssemblyConstraintSet(source)).when(retval).getConstraintSupport();
237 
238     doReturn(flags.values()).when(retval).getFlagInstances();
239     flags.entrySet().forEach(entry -> {
240       assert entry != null;
241       doReturn(entry.getValue()).when(retval).getFlagInstanceByName(eq(entry.getKey().getIndexPosition()));
242     });
243 
244     // Initialize with empty model instances - will be populated later
245     doReturn(CollectionUtil.emptyList()).when(retval).getModelInstances();
246     doReturn(CollectionUtil.emptyMap()).when(retval).getChoiceGroupInstances();
247     doReturn(CollectionUtil.emptyList()).when(retval).getChoiceInstances();
248     doReturn(CollectionUtil.emptyList()).when(retval).getAssemblyInstances();
249     doReturn(CollectionUtil.emptyList()).when(retval).getFieldInstances();
250 
251     return retval;
252   }
253 
254   /**
255    * Resolve model instances for an already-built definition, using the provided
256    * definition maps to resolve references.
257    *
258    * @param definition
259    *          the definition to add model instances to
260    * @param assemblyDefinitions
261    *          map of assembly name to definition for resolving assembly references
262    * @param fieldDefinitions
263    *          map of field name to definition for resolving field references
264    */
265   void resolveModelInstances(
266       @NonNull IAssemblyDefinition definition,
267       @NonNull Map<String, IAssemblyDefinition> assemblyDefinitions,
268       @NonNull Map<String, IFieldDefinition> fieldDefinitions) {
269 
270     ISource source = ObjectUtils.notNull(getSource());
271     List<INamedModelInstanceAbsolute> instances = new ArrayList<>();
272 
273     for (IModelBuilder<?> builder : modelInstances) {
274       INamedModelInstanceAbsolute instance;
275       if (builder instanceof IModelReference) {
276         IModelReference ref = (IModelReference) builder;
277         String refName = ref.getReferencedName();
278 
279         if (builder instanceof AssemblyReference) {
280           IAssemblyDefinition refDef = assemblyDefinitions.get(refName);
281           if (refDef == null) {
282             throw new IllegalStateException("Assembly reference '" + refName + "' not found in module");
283           }
284           // Create an instance that references the existing definition
285           IAssemblyBuilder instanceBuilder = IAssemblyBuilder.builder()
286               .name(refName)
287               .namespace(ObjectUtils.notNull(getNamespace()))
288               .source(source);
289           instance = instanceBuilder.toInstance(definition, refDef);
290         } else if (builder instanceof FieldReference) {
291           IFieldDefinition refDef = fieldDefinitions.get(refName);
292           if (refDef == null) {
293             throw new IllegalStateException("Field reference '" + refName + "' not found in module");
294           }
295           // Create an instance that references the existing definition
296           IFieldBuilder instanceBuilder = IFieldBuilder.builder()
297               .name(refName)
298               .namespace(ObjectUtils.notNull(getNamespace()))
299               .source(source);
300           instance = instanceBuilder.toInstance(definition, refDef);
301         } else {
302           throw new IllegalStateException("Unknown reference type: " + builder.getClass().getName());
303         }
304       } else {
305         // Regular builder - create instance normally
306         instance = builder.source(source).toInstance(definition);
307       }
308       instances.add(instance);
309     }
310 
311     // Wire up the instances
312     Map<IEnhancedQName, INamedModelInstanceAbsolute> instanceMap = instances.stream()
313         .collect(Collectors.toUnmodifiableMap(
314             INamedModelInstanceAbsolute::getQName,
315             Function.identity()));
316 
317     doReturn(new ArrayList<>(instanceMap.values())).when(definition).getModelInstances();
318     instanceMap.forEach((key, value) -> {
319       doReturn(value).when(definition).getNamedModelInstanceByName(eq(key.getIndexPosition()));
320 
321       if (value instanceof IAssemblyInstance) {
322         doReturn(value).when(definition).getAssemblyInstanceByName(eq(key.getIndexPosition()));
323       } else if (value instanceof IFieldInstance) {
324         doReturn(value).when(definition).getFieldInstanceByName(eq(key.getIndexPosition()));
325       }
326     });
327 
328     List<IAssemblyInstance> assemblyInstances = instanceMap.values().stream()
329         .filter(IAssemblyInstance.class::isInstance)
330         .map(IAssemblyInstance.class::cast)
331         .collect(Collectors.toList());
332     doReturn(assemblyInstances).when(definition).getAssemblyInstances();
333 
334     List<IFieldInstance> fieldInstances = instanceMap.values().stream()
335         .filter(IFieldInstance.class::isInstance)
336         .map(IFieldInstance.class::cast)
337         .collect(Collectors.toList());
338     doReturn(fieldInstances).when(definition).getFieldInstances();
339   }
340 
341   @Override
342   protected void applyNamed(INamedModelElement element) {
343     super.applyNamed(element);
344     doReturn(ModelType.ASSEMBLY).when(element).getModelType();
345   }
346 }