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