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}