001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.schemagen;
007
008import java.util.Collection;
009import java.util.HashSet;
010import java.util.LinkedHashMap;
011import java.util.Map;
012import java.util.Set;
013import java.util.concurrent.atomic.AtomicBoolean;
014
015import dev.metaschema.core.model.IAssemblyDefinition;
016import dev.metaschema.core.model.IAssemblyInstance;
017import dev.metaschema.core.model.IChoiceInstance;
018import dev.metaschema.core.model.IDefinition;
019import dev.metaschema.core.model.IFieldDefinition;
020import dev.metaschema.core.model.IFieldInstance;
021import dev.metaschema.core.model.IFlagDefinition;
022import dev.metaschema.core.model.IFlagInstance;
023import dev.metaschema.core.model.IModule;
024import dev.metaschema.core.model.INamedInstance;
025import dev.metaschema.core.model.INamedModelInstance;
026import dev.metaschema.core.model.INamedModelInstanceGrouped;
027import dev.metaschema.core.model.ModelWalker;
028import dev.metaschema.core.util.ObjectUtils;
029import edu.umd.cs.findbugs.annotations.NonNull;
030
031/**
032 * Indexes definitions from a Metaschema module for use in schema generation.
033 * <p>
034 * This class maintains an ordered index of all definitions that are reachable
035 * from root assembly definitions, tracking their reference counts, inline
036 * status, and other usage patterns relevant to schema generation.
037 */
038public class ModuleIndex {
039  // needs to be ordered
040  @SuppressWarnings("PMD.UseConcurrentHashMap")
041  private final Map<IDefinition, DefinitionEntry> index = new LinkedHashMap<>();
042
043  /**
044   * Creates an index of all definitions reachable from the module's root assembly
045   * definitions.
046   *
047   * @param module
048   *          the Metaschema module to index
049   * @param inlineStrategy
050   *          the strategy for determining which definitions should be inlined
051   * @return a new module index containing entries for all reachable definitions
052   */
053  @NonNull
054  public static ModuleIndex indexDefinitions(@NonNull IModule module, @NonNull IInlineStrategy inlineStrategy) {
055    Collection<? extends IAssemblyDefinition> definitions = module.getExportedRootAssemblyDefinitions();
056    ModuleIndex index = new ModuleIndex();
057    if (!definitions.isEmpty()) {
058      IndexVisitor visitor = new IndexVisitor(index, inlineStrategy);
059      for (IAssemblyDefinition definition : definitions) {
060        assert definition != null;
061
062        // // add the root definition to the index
063        // index.getEntry(definition).incrementReferenceCount();
064
065        // walk the definition
066        visitor.walk(ObjectUtils.requireNonNull(definition));
067      }
068    }
069    return index;
070  }
071
072  /**
073   * Checks if an entry exists in this index for the specified definition.
074   *
075   * @param definition
076   *          the definition to check
077   * @return {@code true} if an entry exists for the definition, {@code false}
078   *         otherwise
079   */
080  public boolean hasEntry(@NonNull IDefinition definition) {
081    return index.containsKey(definition);
082  }
083
084  /**
085   * Retrieves or creates the entry for the specified definition.
086   * <p>
087   * If no entry exists for the definition, a new entry is created and added to
088   * the index.
089   *
090   * @param definition
091   *          the definition to get an entry for
092   * @return the existing or newly created entry for the definition
093   */
094  @NonNull
095  public DefinitionEntry getEntry(@NonNull IDefinition definition) {
096    return ObjectUtils.notNull(index.computeIfAbsent(
097        definition,
098        k -> new ModuleIndex.DefinitionEntry(ObjectUtils.notNull(k))));
099  }
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}