001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.oscal.lib.profile.resolver.policy;
007
008import com.vladsch.flexmark.ast.InlineLinkNode;
009import com.vladsch.flexmark.util.ast.Node;
010
011import gov.nist.secauto.metaschema.core.datatype.markup.IMarkupString;
012import gov.nist.secauto.metaschema.core.datatype.markup.flexmark.InsertAnchorExtension;
013import gov.nist.secauto.metaschema.core.datatype.markup.flexmark.InsertAnchorExtension.InsertAnchorNode;
014import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
015import gov.nist.secauto.metaschema.core.metapath.format.IPathFormatter;
016import gov.nist.secauto.metaschema.core.metapath.item.atomic.IMarkupItem;
017import gov.nist.secauto.metaschema.core.metapath.item.node.IAssemblyNodeItem;
018import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
019import gov.nist.secauto.metaschema.core.metapath.item.node.IFieldNodeItem;
020import gov.nist.secauto.metaschema.core.metapath.item.node.IModelNodeItem;
021import gov.nist.secauto.metaschema.core.qname.IEnhancedQName;
022import gov.nist.secauto.metaschema.core.util.CollectionUtil;
023import gov.nist.secauto.metaschema.core.util.ObjectUtils;
024import gov.nist.secauto.oscal.lib.OscalBindingContext;
025import gov.nist.secauto.oscal.lib.OscalModelConstants;
026import gov.nist.secauto.oscal.lib.model.CatalogGroup;
027import gov.nist.secauto.oscal.lib.model.Control;
028import gov.nist.secauto.oscal.lib.model.ControlPart;
029import gov.nist.secauto.oscal.lib.model.Link;
030import gov.nist.secauto.oscal.lib.model.Property;
031import gov.nist.secauto.oscal.lib.model.metadata.AbstractProperty;
032import gov.nist.secauto.oscal.lib.model.metadata.IProperty;
033import gov.nist.secauto.oscal.lib.profile.resolver.support.AbstractCatalogEntityVisitor;
034import gov.nist.secauto.oscal.lib.profile.resolver.support.IEntityItem;
035import gov.nist.secauto.oscal.lib.profile.resolver.support.IIndexer;
036
037import org.apache.logging.log4j.LogManager;
038import org.apache.logging.log4j.Logger;
039
040import java.net.URI;
041import java.util.EnumSet;
042import java.util.HashMap;
043import java.util.HashSet;
044import java.util.Locale;
045import java.util.Map;
046import java.util.Set;
047import java.util.UUID;
048import java.util.function.BiConsumer;
049
050import edu.umd.cs.findbugs.annotations.NonNull;
051import edu.umd.cs.findbugs.annotations.Nullable;
052import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
053
054public final class ReferenceCountingVisitor
055    extends AbstractCatalogEntityVisitor<ReferenceCountingVisitor.Context, Void>
056    implements IReferenceVisitor<ReferenceCountingVisitor.Context> {
057  private static final Logger LOGGER = LogManager.getLogger(ReferenceCountingVisitor.class);
058
059  private static final ReferenceCountingVisitor SINGLETON = new ReferenceCountingVisitor();
060
061  @NonNull
062  private static final MetapathExpression PARAM_MARKUP_METAPATH
063      = MetapathExpression
064          .compile(
065              "label|usage|constraint/(description|tests/remarks)|guideline/prose|select/choice|remarks",
066              OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
067  @NonNull
068  private static final MetapathExpression ROLE_MARKUP_METAPATH
069      = MetapathExpression.compile("title|description|remarks",
070          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
071  @NonNull
072  private static final MetapathExpression LOCATION_MARKUP_METAPATH
073      = MetapathExpression.compile("title|remarks",
074          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
075  @NonNull
076  private static final MetapathExpression PARTY_MARKUP_METAPATH
077      = MetapathExpression.compile("title|remarks",
078          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
079  @NonNull
080  private static final MetapathExpression RESOURCE_MARKUP_METAPATH
081      = MetapathExpression.compile("title|description|remarks",
082          OscalBindingContext.OSCAL_STATIC_METAPATH_CONTEXT);
083
084  @NonNull
085  private static final IReferencePolicy<Property> PROPERTY_POLICY_IGNORE = IReferencePolicy.ignore();
086  @NonNull
087  private static final IReferencePolicy<Link> LINK_POLICY_IGNORE = IReferencePolicy.ignore();
088
089  @NonNull
090  private static final Map<IEnhancedQName, IReferencePolicy<Property>> PROPERTY_POLICIES;
091  @NonNull
092  private static final Map<String, IReferencePolicy<Link>> LINK_POLICIES;
093  @NonNull
094  private static final InsertReferencePolicy INSERT_POLICY = new InsertReferencePolicy();
095  @NonNull
096  private static final AnchorReferencePolicy ANCHOR_POLICY = new AnchorReferencePolicy();
097
098  static {
099    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}