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.MetapathExpression;
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.support.AbstractCatalogEntityVisitor;
34  import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem;
35  import gov.nist.secauto.oscal.lib.profile.resolver.support.IIndexer;
36  
37  import org.apache.logging.log4j.LogManager;
38  import org.apache.logging.log4j.Logger;
39  
40  import java.net.URI;
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 MetapathExpression PARAM_MARKUP_METAPATH
63        = MetapathExpression
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 MetapathExpression ROLE_MARKUP_METAPATH
69        = MetapathExpression.compile("title|description|remarks",
70            OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
71    @NonNull
72    private static final MetapathExpression LOCATION_MARKUP_METAPATH
73        = MetapathExpression.compile("title|remarks",
74            OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
75    @NonNull
76    private static final MetapathExpression PARTY_MARKUP_METAPATH
77        = MetapathExpression.compile("title|remarks",
78            OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
79    @NonNull
80    private static final MetapathExpression RESOURCE_MARKUP_METAPATH
81        = MetapathExpression.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(@NonNull IDocumentNodeItem catalogItem, @NonNull IIndexer indexer, @NonNull URI baseUri) {
169     Context context = new Context(indexer, baseUri);
170     visitCatalog(catalogItem, context);
171 
172     IIndexer index = context.getIndexer();
173     // resolve the entities picked up by the original indexing operation
174     // FIXME: Is this necessary?
175     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.ROLE))
176         .forEachOrdered(
177             item -> resolveEntity(ObjectUtils.notNull(item), context, ReferenceCountingVisitor::resolveRole));
178     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.LOCATION))
179         .forEachOrdered(
180             item -> resolveEntity(ObjectUtils.notNull(item), context,
181                 ReferenceCountingVisitor::resolveLocation));
182     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.PARTY))
183         .forEachOrdered(
184             item -> resolveEntity(ObjectUtils.notNull(item), context,
185                 ReferenceCountingVisitor::resolveParty));
186     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.PARAMETER))
187         .forEachOrdered(
188             item -> resolveEntity(ObjectUtils.notNull(item), context,
189                 ReferenceCountingVisitor::resolveParameter));
190     IIndexer.getReferencedEntitiesAsStream(index.getEntitiesByItemType(IEntityItem.ItemType.RESOURCE))
191         .forEachOrdered(
192             item -> resolveEntity(ObjectUtils.notNull(item), context,
193                 ReferenceCountingVisitor::resolveResource));
194   }
195 
196   @Override
197   public Void visitGroup(
198       IAssemblyNodeItem item,
199       Void childResult,
200       Context context) {
201     IIndexer index = context.getIndexer();
202     // handle the group if it is selected
203     // a group will only be selected if it contains a descendant control that is
204     // selected
205     if (IIndexer.SelectionStatus.SELECTED.equals(index.getSelectionStatus(item))) {
206       CatalogGroup group = ObjectUtils.requireNonNull((CatalogGroup) item.getValue());
207       String id = group.getId();
208 
209       boolean resolve;
210       if (id == null) {
211         // always resolve a group without an identifier
212         resolve = true;
213       } else {
214         IEntityItem entity = index.getEntity(IEntityItem.ItemType.GROUP, id, false);
215         if (entity != null && !context.isResolved(entity)) {
216           // only resolve if not already resolved
217           context.markResolved(entity);
218           resolve = true;
219         } else {
220           resolve = false;
221         }
222       }
223 
224       // resolve only if requested
225       if (resolve) {
226         resolveGroup(item, context);
227       }
228     }
229     return null;
230   }
231 
232   @Override
233   public Void visitControl(
234       IAssemblyNodeItem item,
235       Void childResult,
236       Context context) {
237     IIndexer index = context.getIndexer();
238     // handle the control if it is selected
239     if (IIndexer.SelectionStatus.SELECTED.equals(index.getSelectionStatus(item))) {
240       Control control = ObjectUtils.requireNonNull((Control) item.getValue());
241       IEntityItem entity
242           = context.getIndexer().getEntity(IEntityItem.ItemType.CONTROL, ObjectUtils.notNull(control.getId()), false);
243 
244       // the control must always appear in the index
245       assert entity != null;
246 
247       if (!context.isResolved(entity)) {
248         context.markResolved(entity);
249         if (IIndexer.SelectionStatus.SELECTED.equals(context.getIndexer().getSelectionStatus(item))) {
250           resolveControl(item, context);
251         }
252       }
253     }
254     return null;
255   }
256 
257   @Override
258   protected void visitParts(
259       IAssemblyNodeItem groupOrControlItem,
260       Context context) {
261     // visits all descendant parts
262     CHILD_PART_METAPATH.evaluate(groupOrControlItem).stream()
263         .map(item -> (IAssemblyNodeItem) item)
264         .forEachOrdered(partItem -> {
265           visitPart(ObjectUtils.notNull(partItem), groupOrControlItem, context);
266         });
267   }
268 
269   @Override
270   protected void visitPart(
271       IAssemblyNodeItem item,
272       IAssemblyNodeItem groupOrControlItem,
273       Context context) {
274     assert context != null;
275 
276     ControlPart part = ObjectUtils.requireNonNull((ControlPart) item.getValue());
277     String id = part.getId();
278 
279     boolean resolve;
280     if (id == null) {
281       // always resolve a part without an identifier
282       resolve = true;
283     } else {
284       IEntityItem entity = context.getIndexer().getEntity(IEntityItem.ItemType.PART, id, false);
285       if (entity != null && !context.isResolved(entity)) {
286         // only resolve if not already resolved
287         context.markResolved(entity);
288         resolve = true;
289       } else {
290         resolve = false;
291       }
292     }
293 
294     if (resolve) {
295       resolvePart(item, context);
296     }
297   }
298 
299   protected void resolveGroup(
300       @NonNull IAssemblyNodeItem item,
301       @NonNull Context context) {
302     if (IIndexer.SelectionStatus.SELECTED.equals(context.getIndexer().getSelectionStatus(item))) {
303 
304       // process children
305       item.getModelItemsByName(OscalModelConstants.QNAME_TITLE)
306           .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
307       item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
308           .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
309       item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
310           .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
311 
312       // always visit parts
313       visitParts(item, context);
314 
315       // skip parameters for now. These will be processed by a separate pass.
316     }
317   }
318 
319   protected void resolveControl(
320       @NonNull IAssemblyNodeItem item,
321       @NonNull Context context) {
322     // process non-control, non-param children
323     item.getModelItemsByName(OscalModelConstants.QNAME_TITLE)
324         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
325     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
326         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
327     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
328         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
329 
330     // always visit parts
331     visitParts(item, context);
332 
333     // skip parameters for now. These will be processed by a separate pass.
334   }
335 
336   private static void resolveRole(@NonNull IEntityItem entity, @NonNull Context context) {
337     IModelNodeItem<?, ?> item = entity.getInstance();
338     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
339         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
340     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
341         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
342     ROLE_MARKUP_METAPATH.evaluate(item).getValue()
343         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
344   }
345 
346   private static void resolveParty(@NonNull IEntityItem entity, @NonNull Context context) {
347     IModelNodeItem<?, ?> item = entity.getInstance();
348     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
349         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
350     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
351         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
352     PARTY_MARKUP_METAPATH.evaluate(item).getValue()
353         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
354   }
355 
356   public static void resolveLocation(@NonNull IEntityItem entity, @NonNull Context context) {
357     IModelNodeItem<?, ?> item = entity.getInstance();
358     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
359         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
360     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
361         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
362     LOCATION_MARKUP_METAPATH.evaluate(item).getValue()
363         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
364   }
365 
366   public static void resolveResource(@NonNull IEntityItem entity, @NonNull Context context) {
367     IModelNodeItem<?, ?> item = entity.getInstance();
368 
369     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
370         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
371 
372     item.getModelItemsByName(OscalModelConstants.QNAME_CITATION).forEach(child -> {
373       if (child != null) {
374         child.getModelItemsByName(OscalModelConstants.QNAME_TEXT)
375             .forEach(citationChild -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) citationChild), context));
376         child.getModelItemsByName(OscalModelConstants.QNAME_PROP)
377             .forEach(citationChild -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) citationChild), context));
378         child.getModelItemsByName(OscalModelConstants.QNAME_LINK)
379             .forEach(citationChild -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) citationChild), context));
380       }
381     });
382 
383     RESOURCE_MARKUP_METAPATH.evaluate(item).getValue()
384         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
385   }
386 
387   public static void resolveParameter(@NonNull IEntityItem entity, @NonNull Context context) {
388     IModelNodeItem<?, ?> item = entity.getInstance();
389 
390     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
391         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
392     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
393         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
394     PARAM_MARKUP_METAPATH.evaluate(item).getValue()
395         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
396   }
397 
398   private static void resolvePart(
399       @NonNull IAssemblyNodeItem item,
400       @NonNull Context context) {
401     item.getModelItemsByName(OscalModelConstants.QNAME_TITLE)
402         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
403     item.getModelItemsByName(OscalModelConstants.QNAME_PROP)
404         .forEach(child -> handleProperty(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
405     item.getModelItemsByName(OscalModelConstants.QNAME_LINK)
406         .forEach(child -> handleLink(ObjectUtils.notNull((IAssemblyNodeItem) child), context));
407     item.getModelItemsByName(OscalModelConstants.QNAME_PROSE)
408         .forEach(child -> handleMarkup(ObjectUtils.notNull((IFieldNodeItem) child), context));
409     // item.getModelItemsByName("part").forEach(child ->
410     // visitor.visitPart(ObjectUtils.notNull(child),
411     // context));
412   }
413 
414   private static void handleMarkup(
415       @NonNull IFieldNodeItem item,
416       @NonNull Context context) {
417     IMarkupItem markupItem = (IMarkupItem) item.toAtomicItem();
418     IMarkupString<?> markup = markupItem.asMarkup();
419     handleMarkup(item, markup, context);
420   }
421 
422   private static void handleMarkup(
423       @NonNull IFieldNodeItem contextItem,
424       @NonNull IMarkupString<?> text,
425       @NonNull Context context) {
426     for (Node node : CollectionUtil.toIterable(
427         ObjectUtils.notNull(text.getNodesAsStream().iterator()))) {
428       if (node instanceof InsertAnchorExtension.InsertAnchorNode) {
429         handleInsert(contextItem, (InsertAnchorNode) node, context);
430       } else if (node instanceof InlineLinkNode) {
431         handleAnchor(contextItem, (InlineLinkNode) node, context);
432       }
433     }
434   }
435 
436   private static void handleInsert(
437       @NonNull IFieldNodeItem contextItem,
438       @NonNull InsertAnchorExtension.InsertAnchorNode node,
439       @NonNull Context context) {
440     boolean retval = INSERT_POLICY.handleReference(contextItem, node, context);
441     if (LOGGER.isWarnEnabled() && !retval) {
442       LOGGER.atWarn().log("Unsupported insert type '{}' at '{}'",
443           node.getType().toString(),
444           contextItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
445     }
446   }
447 
448   private static void handleAnchor(
449       @NonNull IFieldNodeItem contextItem,
450       @NonNull InlineLinkNode node,
451       @NonNull Context context) {
452     boolean result = ANCHOR_POLICY.handleReference(contextItem, node, context);
453     if (LOGGER.isWarnEnabled() && !result) {
454       LOGGER.atWarn().log("Unsupported anchor with href '{}' at '{}'",
455           node.getUrl().toString(),
456           contextItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
457     }
458   }
459 
460   private static void handleProperty(
461       @NonNull IAssemblyNodeItem item,
462       @NonNull Context context) {
463     Property property = ObjectUtils.requireNonNull((Property) item.getValue());
464     IEnhancedQName qname = property.getQName();
465 
466     IReferencePolicy<Property> policy = PROPERTY_POLICIES.get(qname);
467 
468     boolean result = policy != null && policy.handleReference(item, property, context);
469     if (LOGGER.isWarnEnabled() && !result) {
470       LOGGER.atWarn().log("Unsupported property '{}' at '{}'",
471           property.getQName(),
472           item.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
473     }
474   }
475 
476   private static void handleLink(
477       @NonNull IAssemblyNodeItem item,
478       @NonNull Context context) {
479     Link link = ObjectUtils.requireNonNull((Link) item.getValue());
480     IReferencePolicy<Link> policy = null;
481     String rel = link.getRel();
482     if (rel != null) {
483       policy = LINK_POLICIES.get(rel);
484     }
485 
486     boolean result = policy != null && policy.handleReference(item, link, context);
487     if (LOGGER.isWarnEnabled() && !result) {
488       LOGGER.atWarn().log("unsupported link rel '{}' at '{}'",
489           link.getRel(),
490           item.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
491     }
492   }
493 
494   protected static void resolveEntity(
495       @NonNull IEntityItem entity,
496       @NonNull Context context,
497       @NonNull BiConsumer<IEntityItem, Context> handler) {
498 
499     if (!context.isResolved(entity)) {
500       context.markResolved(entity);
501 
502       if (LOGGER.isDebugEnabled()) {
503         LOGGER.atDebug().log("Resolving {} identified as '{}'",
504             entity.getItemType().name(),
505             entity.getIdentifier());
506       }
507 
508       if (!IIndexer.SelectionStatus.UNSELECTED
509           .equals(context.getIndexer().getSelectionStatus(entity.getInstance()))) {
510         // only resolve selected and unknown entities
511         handler.accept(entity, context);
512       }
513     }
514   }
515 
516   public void resolveEntity(
517       @NonNull IEntityItem entity,
518       @NonNull Context context) {
519     resolveEntity(entity, context, (theEntity, theContext) -> entityDispatch(
520         ObjectUtils.notNull(theEntity),
521         ObjectUtils.notNull(theContext)));
522   }
523 
524   protected void entityDispatch(@NonNull IEntityItem entity, @NonNull Context context) {
525     IAssemblyNodeItem item = (IAssemblyNodeItem) entity.getInstance();
526     switch (entity.getItemType()) {
527     case CONTROL:
528       resolveControl(item, context);
529       break;
530     case GROUP:
531       resolveGroup(item, context);
532       break;
533     case LOCATION:
534       resolveLocation(entity, context);
535       break;
536     case PARAMETER:
537       resolveParameter(entity, context);
538       break;
539     case PART:
540       resolvePart(item, context);
541       break;
542     case PARTY:
543       resolveParty(entity, context);
544       break;
545     case RESOURCE:
546       resolveResource(entity, context);
547       break;
548     case ROLE:
549       resolveRole(entity, context);
550       break;
551     default:
552       throw new UnsupportedOperationException(entity.getItemType().name());
553     }
554   }
555   //
556   // @Override
557   // protected Void newDefaultResult(Object context) {
558   // return null;
559   // }
560   //
561   // @Override
562   // protected Void aggregateResults(Object first, Object second, Object context)
563   // {
564   // return null;
565   // }
566 
567   public static final class Context {
568     @NonNull
569     private final IIndexer indexer;
570     @NonNull
571     private final URI source;
572     @NonNull
573     private final Set<IEntityItem> resolvedEntities = new HashSet<>();
574 
575     private Context(@NonNull IIndexer indexer, @NonNull URI source) {
576       this.indexer = indexer;
577       this.source = source;
578     }
579 
580     @NonNull
581     @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "intending to expose this field")
582     public IIndexer getIndexer() {
583       return indexer;
584     }
585 
586     @Nullable
587     public IEntityItem getEntity(@NonNull IEntityItem.ItemType itemType, @NonNull String identifier) {
588       return getIndexer().getEntity(itemType, identifier);
589     }
590 
591     @SuppressWarnings("unused")
592     @NonNull
593     private URI getSource() {
594       return source;
595     }
596 
597     public void markResolved(@NonNull IEntityItem entity) {
598       resolvedEntities.add(entity);
599     }
600 
601     public boolean isResolved(@NonNull IEntityItem entity) {
602       return resolvedEntities.contains(entity);
603     }
604 
605     public void incrementReferenceCount(
606         @NonNull IModelNodeItem<?, ?> contextItem,
607         @NonNull IEntityItem.ItemType type,
608         @NonNull UUID identifier) {
609       incrementReferenceCountInternal(
610           contextItem,
611           type,
612           ObjectUtils.notNull(identifier.toString()),
613           false);
614     }
615 
616     public void incrementReferenceCount(
617         @NonNull IModelNodeItem<?, ?> contextItem,
618         @NonNull IEntityItem.ItemType type,
619         @NonNull String identifier) {
620       incrementReferenceCountInternal(
621           contextItem,
622           type,
623           identifier,
624           type.isUuid());
625     }
626 
627     private void incrementReferenceCountInternal(
628         @NonNull IModelNodeItem<?, ?> contextItem,
629         @NonNull IEntityItem.ItemType type,
630         @NonNull String identifier,
631         boolean normalize) {
632       IEntityItem item = getIndexer().getEntity(type, identifier, normalize);
633       if (item == null) {
634         if (LOGGER.isErrorEnabled()) {
635           LOGGER.atError().log("Unknown reference to {} '{}' at '{}'",
636               type.toString().toLowerCase(Locale.ROOT),
637               identifier,
638               contextItem.toPath(IPathFormatter.METAPATH_PATH_FORMATER));
639         }
640       } else {
641         item.incrementReferenceCount();
642       }
643     }
644   }
645 }