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