1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.oscal.lib.profile.resolver.policy;
7   
8   import com.vladsch.flexmark.ast.InlineLinkNode;
9   import com.vladsch.flexmark.util.ast.Node;
10  
11  import org.apache.logging.log4j.LogManager;
12  import org.apache.logging.log4j.Logger;
13  
14  import java.util.EnumSet;
15  import java.util.HashMap;
16  import java.util.HashSet;
17  import java.util.Locale;
18  import java.util.Map;
19  import java.util.Set;
20  import java.util.UUID;
21  import java.util.function.BiConsumer;
22  
23  import dev.metaschema.core.datatype.markup.IMarkupString;
24  import dev.metaschema.core.datatype.markup.flexmark.InsertAnchorExtension;
25  import dev.metaschema.core.datatype.markup.flexmark.InsertAnchorExtension.InsertAnchorNode;
26  import dev.metaschema.core.metapath.IMetapathExpression;
27  import dev.metaschema.core.metapath.format.IPathFormatter;
28  import dev.metaschema.core.metapath.item.atomic.IMarkupItem;
29  import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
30  import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
31  import dev.metaschema.core.metapath.item.node.IFieldNodeItem;
32  import dev.metaschema.core.metapath.item.node.IModelNodeItem;
33  import dev.metaschema.core.qname.IEnhancedQName;
34  import dev.metaschema.core.util.CollectionUtil;
35  import dev.metaschema.core.util.ObjectUtils;
36  import dev.metaschema.oscal.lib.OscalBindingContext;
37  import dev.metaschema.oscal.lib.OscalModelConstants;
38  import dev.metaschema.oscal.lib.model.CatalogGroup;
39  import dev.metaschema.oscal.lib.model.Control;
40  import dev.metaschema.oscal.lib.model.ControlPart;
41  import dev.metaschema.oscal.lib.model.Link;
42  import dev.metaschema.oscal.lib.model.Property;
43  import dev.metaschema.oscal.lib.model.metadata.AbstractProperty;
44  import dev.metaschema.oscal.lib.model.metadata.IProperty;
45  import dev.metaschema.oscal.lib.profile.resolver.ProfileResolver.UriResolver;
46  import dev.metaschema.oscal.lib.profile.resolver.support.AbstractCatalogEntityVisitor;
47  import dev.metaschema.oscal.lib.profile.resolver.support.IEntityItem;
48  import dev.metaschema.oscal.lib.profile.resolver.support.IIndexer;
49  import edu.umd.cs.findbugs.annotations.NonNull;
50  import edu.umd.cs.findbugs.annotations.Nullable;
51  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
52  
53  public final class ReferenceCountingVisitor
54      extends AbstractCatalogEntityVisitor<ReferenceCountingVisitor.Context, Void>
55      implements IReferenceVisitor<ReferenceCountingVisitor.Context> {
56    private static final Logger LOGGER = LogManager.getLogger(ReferenceCountingVisitor.class);
57  
58    private static final ReferenceCountingVisitor SINGLETON = new ReferenceCountingVisitor();
59  
60    @NonNull
61    private static final IMetapathExpression PARAM_MARKUP_METAPATH
62        = IMetapathExpression
63            .compile(
64                "label|usage|constraint/(description|tests/remarks)|guideline/prose|select/choice|remarks",
65                OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
66    @NonNull
67    private static final IMetapathExpression ROLE_MARKUP_METAPATH
68        = IMetapathExpression.compile("title|description|remarks",
69            OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
70    @NonNull
71    private static final IMetapathExpression LOCATION_MARKUP_METAPATH
72        = IMetapathExpression.compile("title|remarks",
73            OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
74    @NonNull
75    private static final IMetapathExpression PARTY_MARKUP_METAPATH
76        = IMetapathExpression.compile("title|remarks",
77            OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
78    @NonNull
79    private static final IMetapathExpression RESOURCE_MARKUP_METAPATH
80        = IMetapathExpression.compile("title|description|remarks",
81            OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
82  
83    @NonNull
84    private static final IReferencePolicy<Property> PROPERTY_POLICY_IGNORE = IReferencePolicy.ignore();
85    @NonNull
86    private static final IReferencePolicy<Link> LINK_POLICY_IGNORE = IReferencePolicy.ignore();
87  
88    @NonNull
89    private static final Map<IEnhancedQName, IReferencePolicy<Property>> PROPERTY_POLICIES;
90    @NonNull
91    private static final Map<String, IReferencePolicy<Link>> LINK_POLICIES;
92    @NonNull
93    private static final InsertReferencePolicy INSERT_POLICY = new InsertReferencePolicy();
94    @NonNull
95    private static final AnchorReferencePolicy ANCHOR_POLICY = new AnchorReferencePolicy();
96  
97    static {
98      PROPERTY_POLICIES = new HashMap<>();
99      PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "resolution-tool"), PROPERTY_POLICY_IGNORE);
100     PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "label"), PROPERTY_POLICY_IGNORE);
101     PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "sort-id"), PROPERTY_POLICY_IGNORE);
102     PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "alt-label"), PROPERTY_POLICY_IGNORE);
103     PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "alt-identifier"), PROPERTY_POLICY_IGNORE);
104     PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "method"), PROPERTY_POLICY_IGNORE);
105     PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.OSCAL_NAMESPACE, "keep"), PROPERTY_POLICY_IGNORE);
106     PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.RMF_NAMESPACE, "method"), PROPERTY_POLICY_IGNORE);
107     PROPERTY_POLICIES.put(AbstractProperty.qname(IProperty.RMF_NAMESPACE, "aggregates"),
108         PropertyReferencePolicy.create(IIdentifierParser.IDENTITY_PARSER, IEntityItem.ItemType.PARAMETER));
109 
110     LINK_POLICIES = new HashMap<>();
111     LINK_POLICIES.put("source-profile", LINK_POLICY_IGNORE);
112     LINK_POLICIES.put("citation", LinkReferencePolicy.create(IEntityItem.ItemType.RESOURCE));
113     LINK_POLICIES.put("reference", LinkReferencePolicy.create(IEntityItem.ItemType.RESOURCE));
114     LINK_POLICIES.put("related", LinkReferencePolicy.create(IEntityItem.ItemType.CONTROL));
115     LINK_POLICIES.put("required", LinkReferencePolicy.create(IEntityItem.ItemType.CONTROL));
116     LINK_POLICIES.put("corresp", LinkReferencePolicy.create(IEntityItem.ItemType.PART));
117   }
118 
119   @SuppressFBWarnings(value = "SING_SINGLETON_GETTER_NOT_SYNCHRONIZED", justification = "class initialization")
120   public static ReferenceCountingVisitor instance() {
121     return SINGLETON;
122   }
123 
124   private ReferenceCountingVisitor() {
125     // visit everything except parts, roles, locations, parties, parameters, and
126     // resources, which are
127     // handled differently by this visitor
128     super(ObjectUtils.notNull(EnumSet.complementOf(
129         EnumSet.of(
130             IEntityItem.ItemType.PART,
131             IEntityItem.ItemType.ROLE,
132             IEntityItem.ItemType.LOCATION,
133             IEntityItem.ItemType.PARTY,
134             IEntityItem.ItemType.PARAMETER,
135             IEntityItem.ItemType.RESOURCE))));
136   }
137 
138   @Override
139   protected Void newDefaultResult(Context context) {
140     // do nothing
141     return null;
142   }
143 
144   @Override
145   protected Void aggregateResults(Void first, Void second, Context context) {
146     // do nothing
147     return null;
148   }
149 
150   //
151   // public void visitProfile(@NonNull Profile profile) {
152   // // process children
153   // Metadata metadata = profile.getMetadata();
154   // if (metadata != null) {
155   // visitMetadata(metadata);
156   // }
157   //
158   // BackMatter backMatter = profile.getBackMatter();
159   // if (backMatter != null) {
160   // for (BackMatter.Resource resource :
161   // CollectionUtil.listOrEmpty(backMatter.getResources())) {
162   // visitResource(resource);
163   // }
164   // }
165   // }
166 
167   public void visitCatalog(
168       @NonNull IDocumentNodeItem catalogItem,
169       @NonNull IIndexer indexer,
170       @NonNull UriResolver resolver) {
171     Context context = new Context(indexer, resolver);
172     visitCatalog(catalogItem, context);
173 
174     IIndexer index = context.getIndexer();
175     // resolve the entities picked up by the original indexing operation
176     // FIXME: Is this necessary?
177     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.ROLE))
178         .forEachOrdered(
179             item -> resolveEntity(ObjectUtils.notNull(item), context, ReferenceCountingVisitor::resolveRole));
180     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.LOCATION))
181         .forEachOrdered(
182             item -> resolveEntity(ObjectUtils.notNull(item), context,
183                 ReferenceCountingVisitor::resolveLocation));
184     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.PARTY))
185         .forEachOrdered(
186             item -> resolveEntity(ObjectUtils.notNull(item), context,
187                 ReferenceCountingVisitor::resolveParty));
188     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.PARAMETER))
189         .forEachOrdered(
190             item -> resolveEntity(ObjectUtils.notNull(item), context,
191                 ReferenceCountingVisitor::resolveParameter));
192     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.RESOURCE))
193         .forEachOrdered(
194             item -> resolveEntity(ObjectUtils.notNull(item), context,
195                 ReferenceCountingVisitor::resolveResource));
196   }
197 
198   @Override
199   public Void visitGroup(
200       IAssemblyNodeItem item,
201       Void childResult,
202       Context context) {
203     IIndexer index = context.getIndexer();
204     // handle the group if it is selected
205     // a group will only be selected if it contains a descendant control that is
206     // selected
207     if (IIndexer.SelectionStatus.SELECTED.equals(index.getSelectionStatus(item))) {
208       CatalogGroup group = ObjectUtils.requireNonNull((CatalogGroup) item.getValue());
209       String id = group.getId();
210 
211       boolean resolve;
212       if (id == null) {
213         // always resolve a group without an identifier
214         resolve = true;
215       } else {
216         IEntityItem entity = index.getEntity(IEntityItem.ItemType.GROUP, id, false);
217         if (entity != null && !context.isResolved(entity)) {
218           // only resolve if not already resolved
219           context.markResolved(entity);
220           resolve = true;
221         } else {
222           resolve = false;
223         }
224       }
225 
226       // resolve only if requested
227       if (resolve) {
228         resolveGroup(item, context);
229       }
230     }
231     return null;
232   }
233 
234   @Override
235   public Void visitControl(
236       IAssemblyNodeItem item,
237       Void childResult,
238       Context context) {
239     IIndexer index = context.getIndexer();
240     // handle the control if it is selected
241     if (IIndexer.SelectionStatus.SELECTED.equals(index.getSelectionStatus(item))) {
242       Control control = ObjectUtils.requireNonNull((Control) item.getValue());
243       IEntityItem entity
244           = context.getIndexer().getEntity(IEntityItem.ItemType.CONTROL, ObjectUtils.notNull(control.getId()), false);
245 
246       // the control must always appear in the index
247       assert entity != null;
248 
249       if (!context.isResolved(entity)) {
250         context.markResolved(entity);
251         if (IIndexer.SelectionStatus.SELECTED.equals(context.getIndexer().getSelectionStatus(item))) {
252           resolveControl(item, context);
253         }
254       }
255     }
256     return null;
257   }
258 
259   @Override
260   protected void visitParts(
261       IAssemblyNodeItem groupOrControlItem,
262       Context context) {
263     // visits all descendant parts
264     CHILD_PART_METAPATH.evaluate(groupOrControlItem).stream()
265         .map(item -> (IAssemblyNodeItem) item)
266         .forEachOrdered(partItem -> {
267           visitPart(ObjectUtils.notNull(partItem), groupOrControlItem, context);
268         });
269   }
270 
271   @Override
272   protected void visitPart(
273       IAssemblyNodeItem item,
274       IAssemblyNodeItem groupOrControlItem,
275       Context context) {
276     assert context != null;
277 
278     ControlPart part = ObjectUtils.requireNonNull((ControlPart) item.getValue());
279     String id = part.getId();
280 
281     boolean resolve;
282     if (id == null) {
283       // always resolve a part without an identifier
284       resolve = true;
285     } else {
286       IEntityItem entity = context.getIndexer().getEntity(IEntityItem.ItemType.PART, id, false);
287       if (entity != null && !context.isResolved(entity)) {
288         // only resolve if not already resolved
289         context.markResolved(entity);
290         resolve = true;
291       } else {
292         resolve = false;
293       }
294     }
295 
296     if (resolve) {
297       resolvePart(item, context);
298     }
299   }
300 
301   protected void resolveGroup(
302       @NonNull IAssemblyNodeItem item,
303       @NonNull Context context) {
304     if (IIndexer.SelectionStatus.SELECTED.equals(context.getIndexer().getSelectionStatus(item))) {
305 
306       // process children
307       item.getModelItemsByName(OscalModelConstants.QNAME_TITLE)
308           .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
309       item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
310           .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
311       item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
312           .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
313 
314       // always visit parts
315       visitParts(item, context);
316 
317       // skip parameters for now. These will be processed by a separate pass.
318     }
319   }
320 
321   protected void resolveControl(
322       @NonNull IAssemblyNodeItem item,
323       @NonNull Context context) {
324     // process non-control, non-param children
325     item.getModelItemsByName(OscalModelConstants.QNAME_TITLE)
326         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
327     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
328         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
329     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
330         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
331 
332     // always visit parts
333     visitParts(item, context);
334 
335     // skip parameters for now. These will be processed by a separate pass.
336   }
337 
338   private static void resolveRole(@NonNull IEntityItem entity, @NonNull Context context) {
339     IModelNodeItem<?, ?> item = entity.getInstance();
340     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
341         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
342     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
343         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
344     ROLE_MARKUP_METAPATH.evaluate(item)
345         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
346   }
347 
348   private static void resolveParty(@NonNull IEntityItem entity, @NonNull Context context) {
349     IModelNodeItem<?, ?> item = entity.getInstance();
350     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
351         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
352     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
353         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
354     PARTY_MARKUP_METAPATH.evaluate(item)
355         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
356   }
357 
358   public static void resolveLocation(@NonNull IEntityItem entity, @NonNull Context context) {
359     IModelNodeItem<?, ?> item = entity.getInstance();
360     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
361         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
362     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
363         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
364     LOCATION_MARKUP_METAPATH.evaluate(item)
365         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
366   }
367 
368   public static void resolveResource(@NonNull IEntityItem entity, @NonNull Context context) {
369     IModelNodeItem<?, ?> item = entity.getInstance();
370 
371     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
372         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
373 
374     item.getModelItemsByName(OscalModelConstants.QNAME_CITATION).forEach(child -> {
375       if (child != null) {
376         child.getModelItemsByName(OscalModelConstants.QNAME_TEXT)
377             .forEach(citationChild -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) citationChild), context));
378         child.getModelItemsByName(OscalModelConstants.QNAME_PROP)
379             .forEach(citationChild -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) citationChild), context));
380         child.getModelItemsByName(OscalModelConstants.QNAME_LINK)
381             .forEach(citationChild -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) citationChild), context));
382       }
383     });
384 
385     RESOURCE_MARKUP_METAPATH.evaluate(item)
386         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
387   }
388 
389   public static void resolveParameter(@NonNull IEntityItem entity, @NonNull Context context) {
390     IModelNodeItem<?, ?> item = entity.getInstance();
391 
392     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
393         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
394     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
395         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
396     PARAM_MARKUP_METAPATH.evaluate(item)
397         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
398   }
399 
400   private static void resolvePart(
401       @NonNull IAssemblyNodeItem item,
402       @NonNull Context context) {
403     item.getModelItemsByName(OscalModelConstants.QNAME_TITLE)
404         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
405     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
406         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
407     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
408         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
409     item.getModelItemsByName(OscalModelConstants.QNAME_PROSE)
410         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
411     // item.getModelItemsByName("part").forEach(child ->
412     // visitor.visitPart(ObjectUtils.notNull(child),
413     // context));
414   }
415 
416   private static void handleMarkup(
417       @NonNull IFieldNodeItem item,
418       @NonNull Context context) {
419     IMarkupItem markupItem = (IMarkupItem) item.toAtomicItem();
420     IMarkupString<?> markup = markupItem.asMarkup();
421     handleMarkup(item, markup, context);
422   }
423 
424   private static void handleMarkup(
425       @NonNull IFieldNodeItem contextItem,
426       @NonNull IMarkupString<?> text,
427       @NonNull Context context) {
428     for (Node node : CollectionUtil.toIterable(
429         ObjectUtils.notNull(text.getNodesAsStream().iterator()))) {
430       if (node instanceof InsertAnchorExtension.InsertAnchorNode) {
431         handleInsert(contextItem, (InsertAnchorNode) node, context);
432       } else if (node instanceof InlineLinkNode) {
433         handleAnchor(contextItem, (InlineLinkNode) node, context);
434       }
435     }
436   }
437 
438   private static void handleInsert(
439       @NonNull IFieldNodeItem contextItem,
440       @NonNull InsertAnchorExtension.InsertAnchorNode node,
441       @NonNull Context context) {
442     boolean retval = INSERT_POLICY.handleReference(contextItem, node, context);
443     if (LOGGER.isDebugEnabled() && !retval) {
444       LOGGER.atDebug().log("Unsupported insert type '{}' at '{}'",
445           node.getType().toString(),
446           contextItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
447     }
448   }
449 
450   private static void handleAnchor(
451       @NonNull IFieldNodeItem contextItem,
452       @NonNull InlineLinkNode node,
453       @NonNull Context context) {
454     boolean result = ANCHOR_POLICY.handleReference(contextItem, node, context);
455     if (LOGGER.isDebugEnabled() && !result) {
456       LOGGER.atDebug().log("Unsupported anchor with href '{}' at '{}'",
457           node.getUrl().toString(),
458           contextItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
459     }
460   }
461 
462   private static void handleProperty(
463       @NonNull IAssemblyNodeItem item,
464       @NonNull Context context) {
465     Property property = ObjectUtils.requireNonNull((Property) item.getValue());
466     IEnhancedQName qname = property.getQName();
467 
468     IReferencePolicy<Property> policy = PROPERTY_POLICIES.get(qname);
469 
470     boolean result = policy != null && policy.handleReference(item, property, context);
471     if (LOGGER.isDebugEnabled() && !result) {
472       LOGGER.atDebug().log("Unsupported property '{}' at '{}'",
473           property.getQName(),
474           item.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
475     }
476   }
477 
478   private static void handleLink(
479       @NonNull IAssemblyNodeItem item,
480       @NonNull Context context) {
481     Link link = ObjectUtils.requireNonNull((Link) item.getValue());
482     IReferencePolicy<Link> policy = null;
483     String rel = link.getRel();
484     if (rel != null) {
485       policy = LINK_POLICIES.get(rel);
486     }
487 
488     boolean result = policy != null && policy.handleReference(item, link, context);
489     if (LOGGER.isDebugEnabled() && !result) {
490       LOGGER.atDebug().log("unsupported link rel '{}' at '{}'",
491           link.getRel(),
492           item.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
493     }
494   }
495 
496   protected static void resolveEntity(
497       @NonNull IEntityItem entity,
498       @NonNull Context context,
499       @NonNull BiConsumer<IEntityItem, Context> handler) {
500 
501     if (!context.isResolved(entity)) {
502       context.markResolved(entity);
503 
504       if (LOGGER.isDebugEnabled()) {
505         LOGGER.atDebug().log("Resolving {} identified as '{}'",
506             entity.getItemType().name(),
507             entity.getIdentifier());
508       }
509 
510       if (!IIndexer.SelectionStatus.UNSELECTED
511           .equals(context.getIndexer().getSelectionStatus(entity.getInstance()))) {
512         // only resolve selected and unknown entities
513         handler.accept(entity, context);
514       }
515     }
516   }
517 
518   public void resolveEntity(
519       @NonNull IEntityItem entity,
520       @NonNull Context context) {
521     resolveEntity(entity, context, (theEntity, theContext) -> entityDispatch(
522         ObjectUtils.notNull(theEntity),
523         ObjectUtils.notNull(theContext)));
524   }
525 
526   protected void entityDispatch(@NonNull IEntityItem entity, @NonNull Context context) {
527     IAssemblyNodeItem item = (IAssemblyNodeItem) entity.getInstance();
528     switch (entity.getItemType()) {
529     case CONTROL:
530       resolveControl(item, context);
531       break;
532     case GROUP:
533       resolveGroup(item, context);
534       break;
535     case LOCATION:
536       resolveLocation(entity, context);
537       break;
538     case PARAMETER:
539       resolveParameter(entity, context);
540       break;
541     case PART:
542       resolvePart(item, context);
543       break;
544     case PARTY:
545       resolveParty(entity, context);
546       break;
547     case RESOURCE:
548       resolveResource(entity, context);
549       break;
550     case ROLE:
551       resolveRole(entity, context);
552       break;
553     default:
554       throw new UnsupportedOperationException(entity.getItemType().name());
555     }
556   }
557   //
558   // @Override
559   // protected Void newDefaultResult(Object context) {
560   // return null;
561   // }
562   //
563   // @Override
564   // protected Void aggregateResults(Object first, Object second, Object context)
565   // {
566   // return null;
567   // }
568 
569   public static final class Context {
570     @NonNull
571     private final IIndexer indexer;
572     @NonNull
573     private final UriResolver resolver;
574     @NonNull
575     private final Set<IEntityItem> resolvedEntities = new HashSet<>();
576 
577     private Context(@NonNull IIndexer indexer, @NonNull UriResolver resolver) {
578       this.indexer = indexer;
579       this.resolver = resolver;
580     }
581 
582     @NonNull
583     @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "intending to expose this field")
584     public IIndexer getIndexer() {
585       return indexer;
586     }
587 
588     @NonNull
589     public UriResolver getUriResolver() {
590       return resolver;
591     }
592 
593     @Nullable
594     public IEntityItem getEntity(@NonNull IEntityItem.ItemType itemType, @NonNull String identifier) {
595       return getIndexer().getEntity(itemType, identifier);
596     }
597 
598     public void markResolved(@NonNull IEntityItem entity) {
599       resolvedEntities.add(entity);
600     }
601 
602     public boolean isResolved(@NonNull IEntityItem entity) {
603       return resolvedEntities.contains(entity);
604     }
605 
606     public void incrementReferenceCount(
607         @NonNull IModelNodeItem<?, ?> contextItem,
608         @NonNull IEntityItem.ItemType type,
609         @NonNull UUID identifier) {
610       incrementReferenceCountInternal(
611           contextItem,
612           type,
613           ObjectUtils.notNull(identifier.toString()),
614           false);
615     }
616 
617     public void incrementReferenceCount(
618         @NonNull IModelNodeItem<?, ?> contextItem,
619         @NonNull IEntityItem.ItemType type,
620         @NonNull String identifier) {
621       incrementReferenceCountInternal(
622           contextItem,
623           type,
624           identifier,
625           type.isUuid());
626     }
627 
628     private void incrementReferenceCountInternal(
629         @NonNull IModelNodeItem<?, ?> contextItem,
630         @NonNull IEntityItem.ItemType type,
631         @NonNull String identifier,
632         boolean normalize) {
633       IEntityItem item = getIndexer().getEntity(type, identifier, normalize);
634       if (item == null) {
635         if (LOGGER.isErrorEnabled()) {
636           LOGGER.atError().log("Unknown reference to {} '{}' at '{}'",
637               type.toString().toLowerCase(Locale.ROOT),
638               identifier,
639               contextItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
640         }
641       } else {
642         item.incrementReferenceCount();
643       }
644     }
645   }
646 }