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