001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.oscal.lib.metapath.function.library;
007
008import java.net.URI;
009import java.util.List;
010
011import dev.metaschema.core.metapath.DynamicContext;
012import dev.metaschema.core.metapath.MetapathConstants;
013import dev.metaschema.core.metapath.function.FunctionUtils;
014import dev.metaschema.core.metapath.function.IArgument;
015import dev.metaschema.core.metapath.function.IFunction;
016import dev.metaschema.core.metapath.function.InvalidTypeFunctionException;
017import dev.metaschema.core.metapath.item.IItem;
018import dev.metaschema.core.metapath.item.ISequence;
019import dev.metaschema.core.metapath.item.atomic.IAnyUriItem;
020import dev.metaschema.core.metapath.item.atomic.IBooleanItem;
021import dev.metaschema.core.metapath.item.atomic.IStringItem;
022import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
023import dev.metaschema.core.metapath.item.node.IFieldNodeItem;
024import dev.metaschema.core.metapath.item.node.IFlagNodeItem;
025import dev.metaschema.core.metapath.item.node.IModelNodeItem;
026import dev.metaschema.core.metapath.type.InvalidTypeMetapathException;
027import dev.metaschema.core.model.IFlagInstance;
028import dev.metaschema.core.model.IModelDefinition;
029import dev.metaschema.core.qname.IEnhancedQName;
030import dev.metaschema.core.util.ObjectUtils;
031import dev.metaschema.oscal.lib.OscalModelConstants;
032import dev.metaschema.oscal.lib.model.metadata.IProperty;
033import edu.umd.cs.findbugs.annotations.NonNull;
034
035public final class HasOscalNamespace {
036  @NonNull
037  private static final IEnhancedQName NS_FLAG_QNAME = IEnhancedQName.of("ns");
038  @NonNull
039  static final IFunction SIGNATURE_ONE_ARG = IFunction.builder()
040      .name("has-oscal-namespace")
041      .namespace(OscalModelConstants.NS_OSCAL)
042      .argument(IArgument.builder()
043          .name("namespace")
044          .type(IStringItem.type())
045          .oneOrMore()
046          .build())
047      .allowUnboundedArity(true)
048      .returnType(IBooleanItem.type())
049      .focusDependent()
050      .contextIndependent()
051      .deterministic()
052      .returnOne()
053      .functionHandler(HasOscalNamespace::executeOneArg)
054      .build();
055
056  @NonNull
057  static final IFunction SIGNATURE_TWO_ARGS = IFunction.builder()
058      .name("has-oscal-namespace")
059      .namespace(OscalModelConstants.NS_OSCAL)
060      .argument(IArgument.builder()
061          .name("propOrPart")
062          .type(IAssemblyNodeItem.type())
063          .one()
064          .build())
065      .argument(IArgument.builder()
066          .name("namespace")
067          .type(IStringItem.type())
068          .oneOrMore()
069          .build())
070      .allowUnboundedArity(true)
071      .focusIndependent()
072      .contextIndependent()
073      .deterministic()
074      .returnType(IBooleanItem.type())
075      .returnOne()
076      .functionHandler(HasOscalNamespace::executeTwoArg)
077      .build();
078
079  @NonNull
080  static final IFunction SIGNATURE_ONE_ARG_METAPATH = IFunction.builder()
081      .name("has-oscal-namespace")
082      .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS)
083      .argument(IArgument.builder()
084          .name("namespace")
085          .type(IStringItem.type())
086          .oneOrMore()
087          .build())
088      .allowUnboundedArity(true)
089      .returnType(IBooleanItem.type())
090      .focusDependent()
091      .contextIndependent()
092      .deterministic()
093      .returnOne()
094      .functionHandler(HasOscalNamespace::executeOneArg)
095      .build();
096
097  @NonNull
098  static final IFunction SIGNATURE_TWO_ARGS_METAPATH = IFunction.builder()
099      .name("has-oscal-namespace")
100      .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS)
101      .argument(IArgument.builder()
102          .name("propOrPart")
103          .type(IAssemblyNodeItem.type())
104          .one()
105          .build())
106      .argument(IArgument.builder()
107          .name("namespace")
108          .type(IStringItem.type())
109          .oneOrMore()
110          .build())
111      .allowUnboundedArity(true)
112      .focusIndependent()
113      .contextIndependent()
114      .deterministic()
115      .returnType(IBooleanItem.type())
116      .returnOne()
117      .functionHandler(HasOscalNamespace::executeTwoArg)
118      .build();
119
120  private HasOscalNamespace() {
121    // disable construction
122  }
123
124  @SuppressWarnings({ "unused",
125      "PMD.OnlyOneReturn" // readability
126  })
127  @NonNull
128  public static ISequence<?> executeOneArg(
129      @NonNull IFunction function,
130      @NonNull List<ISequence<?>> arguments,
131      @NonNull DynamicContext dynamicContext,
132      IItem focus) {
133    assert arguments.size() == 1;
134    ISequence<? extends IStringItem> namespaceArgs = FunctionUtils.asType(
135        ObjectUtils.notNull(arguments.get(0)));
136
137    if (namespaceArgs.isEmpty()) {
138      return ISequence.empty();
139    }
140
141    // Support both assembly and field nodes
142    IModelNodeItem<?, ?> node;
143    if (focus instanceof IAssemblyNodeItem) {
144      node = (IAssemblyNodeItem) focus;
145    } else if (focus instanceof IFieldNodeItem) {
146      node = (IFieldNodeItem) focus;
147    } else {
148      throw new InvalidTypeMetapathException(
149          focus,
150          String.format("Expected an assembly or field node, but got '%s'",
151              focus == null ? "null" : focus.getClass().getName()));
152    }
153    return ISequence.of(hasNamespace(node, namespaceArgs));
154  }
155
156  @SuppressWarnings({ "unused",
157      "PMD.OnlyOneReturn" // readability
158  })
159  @NonNull
160  public static ISequence<?> executeTwoArg(
161      @NonNull IFunction function,
162      @NonNull List<ISequence<?>> arguments,
163      @NonNull DynamicContext dynamicContext,
164      IItem focus) {
165    assert arguments.size() == 2;
166
167    ISequence<? extends IStringItem> namespaceArgs = FunctionUtils.asType(
168        ObjectUtils.notNull(arguments.get(1)));
169    if (namespaceArgs.isEmpty()) {
170      return ISequence.empty();
171    }
172
173    ISequence<? extends IAssemblyNodeItem> nodeSequence = FunctionUtils.asType(
174        ObjectUtils.notNull(arguments.get(0)));
175
176    // always not null, since the first item is required
177    IAssemblyNodeItem node = FunctionUtils.asType(ObjectUtils.requireNonNull(nodeSequence.getFirstItem(true)));
178    return ISequence.of(hasNamespace(node, namespaceArgs));
179  }
180
181  @SuppressWarnings("PMD.LinguisticNaming") // false positive
182  @NonNull
183  public static IBooleanItem hasNamespace(
184      @NonNull IModelNodeItem<?, ?> propOrPart,
185      @NonNull ISequence<? extends IStringItem> namespaces) {
186    Object propOrPartObject = propOrPart.getValue();
187    if (propOrPartObject == null) {
188      throw new InvalidTypeFunctionException(InvalidTypeFunctionException.NODE_HAS_NO_TYPED_VALUE, propOrPart);
189    }
190
191    URI nodeNamespace = null;
192    // get the "ns" flag value
193    IFlagNodeItem ns = propOrPart.getFlagByName(NS_FLAG_QNAME);
194    if (ns == null) {
195      // check if the node actually has a "ns" flag
196      IModelDefinition definition = propOrPart.getDefinition();
197      IFlagInstance flag = definition.getFlagInstanceByName(NS_FLAG_QNAME.getIndexPosition());
198      if (flag == null) {
199        // Node doesn't support namespaces, so it can't match any namespace
200        return IBooleanItem.FALSE;
201      }
202
203      Object defaultValue = flag.getDefinition().getDefaultValue();
204      if (defaultValue != null) {
205        nodeNamespace = IAnyUriItem.valueOf(ObjectUtils.notNull(defaultValue.toString())).asUri();
206      }
207    } else {
208      nodeNamespace = IAnyUriItem.cast(ObjectUtils.notNull(ns.toAtomicItem())).asUri();
209    }
210
211    String nodeNamespaceString = IProperty.normalizeNamespace(nodeNamespace).toString();
212    return IBooleanItem.valueOf(namespaces.stream()
213        .map(node -> nodeNamespaceString.equals(node.asString()))
214        .anyMatch(bool -> bool));
215  }
216}