1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.schemagen;
7   
8   import java.util.Collection;
9   import java.util.HashSet;
10  import java.util.LinkedHashMap;
11  import java.util.Map;
12  import java.util.Set;
13  import java.util.concurrent.atomic.AtomicBoolean;
14  
15  import dev.metaschema.core.model.IAssemblyDefinition;
16  import dev.metaschema.core.model.IAssemblyInstance;
17  import dev.metaschema.core.model.IChoiceInstance;
18  import dev.metaschema.core.model.IDefinition;
19  import dev.metaschema.core.model.IFieldDefinition;
20  import dev.metaschema.core.model.IFieldInstance;
21  import dev.metaschema.core.model.IFlagDefinition;
22  import dev.metaschema.core.model.IFlagInstance;
23  import dev.metaschema.core.model.IModule;
24  import dev.metaschema.core.model.INamedInstance;
25  import dev.metaschema.core.model.INamedModelInstance;
26  import dev.metaschema.core.model.INamedModelInstanceGrouped;
27  import dev.metaschema.core.model.ModelWalker;
28  import dev.metaschema.core.util.ObjectUtils;
29  import edu.umd.cs.findbugs.annotations.NonNull;
30  
31  /**
32   * Indexes definitions from a Metaschema module for use in schema generation.
33   * <p>
34   * This class maintains an ordered index of all definitions that are reachable
35   * from root assembly definitions, tracking their reference counts, inline
36   * status, and other usage patterns relevant to schema generation.
37   */
38  public class ModuleIndex {
39    // needs to be ordered
40    @SuppressWarnings("PMD.UseConcurrentHashMap")
41    private final Map<IDefinition, DefinitionEntry> index = new LinkedHashMap<>();
42  
43    /**
44     * Creates an index of all definitions reachable from the module's root assembly
45     * definitions.
46     *
47     * @param module
48     *          the Metaschema module to index
49     * @param inlineStrategy
50     *          the strategy for determining which definitions should be inlined
51     * @return a new module index containing entries for all reachable definitions
52     */
53    @NonNull
54    public static ModuleIndex indexDefinitions(@NonNull IModule module, @NonNull IInlineStrategy inlineStrategy) {
55      Collection<? extends IAssemblyDefinition> definitions = module.getExportedRootAssemblyDefinitions();
56      ModuleIndex index = new ModuleIndex();
57      if (!definitions.isEmpty()) {
58        IndexVisitor visitor = new IndexVisitor(index, inlineStrategy);
59        for (IAssemblyDefinition definition : definitions) {
60          assert definition != null;
61  
62          // // add the root definition to the index
63          // index.getEntry(definition).incrementReferenceCount();
64  
65          // walk the definition
66          visitor.walk(ObjectUtils.requireNonNull(definition));
67        }
68      }
69      return index;
70    }
71  
72    /**
73     * Checks if an entry exists in this index for the specified definition.
74     *
75     * @param definition
76     *          the definition to check
77     * @return {@code true} if an entry exists for the definition, {@code false}
78     *         otherwise
79     */
80    public boolean hasEntry(@NonNull IDefinition definition) {
81      return index.containsKey(definition);
82    }
83  
84    /**
85     * Retrieves or creates the entry for the specified definition.
86     * <p>
87     * If no entry exists for the definition, a new entry is created and added to
88     * the index.
89     *
90     * @param definition
91     *          the definition to get an entry for
92     * @return the existing or newly created entry for the definition
93     */
94    @NonNull
95    public DefinitionEntry getEntry(@NonNull IDefinition definition) {
96      return ObjectUtils.notNull(index.computeIfAbsent(
97          definition,
98          k -> new ModuleIndex.DefinitionEntry(ObjectUtils.notNull(k))));
99    }
100 
101   /**
102    * Retrieves all definition entries in this index.
103    *
104    * @return an unmodifiable collection of all definition entries, in insertion
105    *         order
106    */
107   @NonNull
108   public Collection<DefinitionEntry> getDefinitions() {
109     return ObjectUtils.notNull(index.values());
110   }
111 
112   private static class IndexVisitor
113       extends ModelWalker<ModuleIndex> {
114     @NonNull
115     private final IInlineStrategy inlineStrategy;
116     @NonNull
117     private final ModuleIndex index;
118 
119     public IndexVisitor(@NonNull ModuleIndex index, @NonNull IInlineStrategy inlineStrategy) {
120       this.index = index;
121       this.inlineStrategy = inlineStrategy;
122     }
123 
124     @Override
125     protected ModuleIndex getDefaultData() {
126       return index;
127     }
128 
129     @Override
130     protected boolean visit(IFlagInstance instance, ModuleIndex index) {
131       handleInstance(instance);
132       return true;
133     }
134 
135     @Override
136     protected boolean visit(IFieldInstance instance, ModuleIndex index) {
137       handleInstance(instance);
138       return true;
139     }
140 
141     @Override
142     protected boolean visit(IAssemblyInstance instance, ModuleIndex index) {
143       handleInstance(instance);
144       return true;
145     }
146 
147     @Override
148     protected void visit(IFlagDefinition def, ModuleIndex data) {
149       handleDefinition(def);
150     }
151 
152     // @Override
153     // protected boolean visit(IAssemblyDefinition def, ModuleIndex data) {
154     // // only walk if the definition hasn't already been visited
155     // return !index.hasEntry(def);
156     // }
157 
158     @Override
159     protected boolean visit(IFieldDefinition def, ModuleIndex data) {
160       return handleDefinition(def);
161     }
162 
163     @Override
164     protected boolean visit(IAssemblyDefinition def, ModuleIndex data) {
165       return handleDefinition(def);
166     }
167 
168     private boolean handleDefinition(@NonNull IDefinition definition) {
169       DefinitionEntry entry = getDefaultData().getEntry(definition);
170       boolean visited = entry.isVisited();
171       if (!visited) {
172         entry.markVisited();
173 
174         if (inlineStrategy.isInline(definition, index)) {
175           entry.markInline();
176         }
177       }
178       return !visited;
179     }
180 
181     /**
182      * Updates the index entry for the definition associated with the reference.
183      *
184      * @param instance
185      *          the instance to process
186      */
187     @NonNull
188     private DefinitionEntry handleInstance(INamedInstance instance) {
189       IDefinition definition = instance.getDefinition();
190       // check if this will be a new entry, which needs to be called before getEntry,
191       // which will create it
192       DefinitionEntry entry = getDefaultData().getEntry(definition);
193       entry.addReference(instance);
194 
195       if (isChoice(instance)) {
196         entry.markUsedAsChoice();
197       }
198 
199       if (isChoiceSibling(instance)) {
200         entry.markAsChoiceSibling();
201       }
202       return entry;
203     }
204 
205     private static boolean isChoice(@NonNull INamedInstance instance) {
206       return instance.getParentContainer() instanceof IChoiceInstance;
207     }
208 
209     private static boolean isChoiceSibling(@NonNull INamedInstance instance) {
210       IDefinition containingDefinition = instance.getContainingDefinition();
211       return containingDefinition instanceof IAssemblyDefinition
212           && !((IAssemblyDefinition) containingDefinition).getChoiceInstances().isEmpty();
213     }
214   }
215 
216   /**
217    * Represents an entry in the module index for a single definition.
218    * <p>
219    * Each entry tracks usage information about a definition including its
220    * references, inline status, and how it is used within choice groups.
221    */
222   public static class DefinitionEntry {
223     @NonNull
224     private final IDefinition definition;
225     private final Set<INamedInstance> references = new HashSet<>();
226     private final AtomicBoolean inline = new AtomicBoolean(); // false
227     private final AtomicBoolean visited = new AtomicBoolean(); // false
228     private final AtomicBoolean usedAsChoice = new AtomicBoolean(); // false
229     private final AtomicBoolean choiceSibling = new AtomicBoolean(); // false
230 
231     /**
232      * Constructs a new definition entry for the specified definition.
233      *
234      * @param definition
235      *          the definition this entry represents
236      */
237     public DefinitionEntry(@NonNull IDefinition definition) {
238       this.definition = definition;
239     }
240 
241     /**
242      * Retrieves the definition associated with this entry.
243      *
244      * @return the definition
245      */
246     @NonNull
247     public IDefinition getDefinition() {
248       return definition;
249     }
250 
251     /**
252      * Checks if this definition is a root assembly definition.
253      *
254      * @return {@code true} if the definition is a root assembly, {@code false}
255      *         otherwise
256      */
257     public boolean isRoot() {
258       return definition instanceof IAssemblyDefinition
259           && ((IAssemblyDefinition) definition).isRoot();
260     }
261 
262     /**
263      * Checks if this definition is referenced by any instance or is a root
264      * definition.
265      *
266      * @return {@code true} if the definition has references or is a root,
267      *         {@code false} otherwise
268      */
269     public boolean isReferenced() {
270       return !references.isEmpty()
271           || isRoot();
272     }
273 
274     /**
275      * Retrieves all instances that reference this definition.
276      *
277      * @return a set of referencing instances
278      */
279     public Set<INamedInstance> getReferences() {
280       return references;
281     }
282 
283     /**
284      * Adds a reference to this definition from the specified instance.
285      *
286      * @param reference
287      *          the instance referencing this definition
288      * @return {@code true} if the reference was added, {@code false} if it already
289      *         existed
290      */
291     public boolean addReference(@NonNull INamedInstance reference) {
292       return references.add(reference);
293     }
294 
295     /**
296      * Marks this definition as having been visited during indexing.
297      */
298     public void markVisited() {
299       visited.compareAndSet(false, true);
300     }
301 
302     /**
303      * Checks if this definition has been visited during indexing.
304      *
305      * @return {@code true} if the definition was visited, {@code false} otherwise
306      */
307     public boolean isVisited() {
308       return visited.get();
309     }
310 
311     /**
312      * Marks this definition as being inlined in the generated schema.
313      */
314     public void markInline() {
315       inline.compareAndSet(false, true);
316     }
317 
318     /**
319      * Checks if this definition should be inlined in the generated schema.
320      *
321      * @return {@code true} if the definition is inlined, {@code false} otherwise
322      */
323     public boolean isInline() {
324       return inline.get();
325     }
326 
327     /**
328      * Marks this definition as being used within a choice group.
329      */
330     public void markUsedAsChoice() {
331       usedAsChoice.compareAndSet(false, true);
332     }
333 
334     /**
335      * Checks if this definition is used within a choice group.
336      *
337      * @return {@code true} if the definition is used as a choice, {@code false}
338      *         otherwise
339      */
340     public boolean isUsedAsChoice() {
341       return usedAsChoice.get();
342     }
343 
344     /**
345      * Marks this definition as having sibling elements in a choice group.
346      */
347     public void markAsChoiceSibling() {
348       choiceSibling.compareAndSet(false, true);
349     }
350 
351     /**
352      * Checks if this definition has sibling elements in a choice group.
353      *
354      * @return {@code true} if the definition is a choice sibling, {@code false}
355      *         otherwise
356      */
357     public boolean isChoiceSibling() {
358       return choiceSibling.get();
359     }
360 
361     /**
362      * Checks if any reference to this definition uses a JSON key flag.
363      *
364      * @return {@code true} if any reference has a JSON key, {@code false} otherwise
365      */
366     public boolean isUsedAsJsonKey() {
367       return references.stream()
368           .anyMatch(ref -> ref instanceof INamedModelInstance
369               && ((INamedModelInstance) ref).hasJsonKey());
370     }
371 
372     /**
373      * Checks if this definition is used without a JSON key flag or is a flag
374      * definition.
375      *
376      * @return {@code true} if the definition is a flag or has any references
377      *         without a JSON key, {@code false} otherwise
378      */
379     public boolean isUsedWithoutJsonKey() {
380       return definition instanceof IFlagDefinition
381           || references.isEmpty()
382           || references.stream()
383               .anyMatch(ref -> ref instanceof INamedModelInstance
384                   && !((INamedModelInstance) ref).hasJsonKey());
385     }
386 
387     /**
388      * Checks if this definition is a member of a choice group.
389      *
390      * @return {@code true} if any reference is a grouped model instance,
391      *         {@code false} otherwise
392      */
393     public boolean isChoiceGroupMember() {
394       return references.stream()
395           .anyMatch(INamedModelInstanceGrouped.class::isInstance);
396     }
397   }
398 }