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