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.function.FunctionUtils;
013import dev.metaschema.core.metapath.function.IArgument;
014import dev.metaschema.core.metapath.function.IFunction;
015import dev.metaschema.core.metapath.function.InvalidArgumentFunctionException;
016import dev.metaschema.core.metapath.function.UnidentifiedFunctionError;
017import dev.metaschema.core.metapath.function.library.FnRoot;
018import dev.metaschema.core.metapath.item.IItem;
019import dev.metaschema.core.metapath.item.ISequence;
020import dev.metaschema.core.metapath.item.atomic.IAnyUriItem;
021import dev.metaschema.core.metapath.item.atomic.IStringItem;
022import dev.metaschema.core.metapath.item.atomic.IUuidItem;
023import dev.metaschema.core.metapath.item.node.INodeItem;
024import dev.metaschema.core.util.ObjectUtils;
025import dev.metaschema.oscal.lib.OscalModelConstants;
026import dev.metaschema.oscal.lib.OscalUtils;
027import dev.metaschema.oscal.lib.model.BackMatter.Resource;
028import dev.metaschema.oscal.lib.model.BackMatter.Resource.Rlink;
029import dev.metaschema.oscal.lib.model.IOscalInstance;
030import edu.umd.cs.findbugs.annotations.NonNull;
031import edu.umd.cs.findbugs.annotations.Nullable;
032
033/**
034 * Supports resolving a link to a backmatter resource.
035 */
036public final class ResolveReference {
037  private static final String NAME = "resolve-reference";
038
039  @NonNull
040  static final IFunction SIGNATURE_ONE_ARG = IFunction.builder()
041      .name(NAME)
042      .namespace(OscalModelConstants.NS_OSCAL)
043      .argument(IArgument.builder()
044          .name("uri")
045          .type(IAnyUriItem.type())
046          .zeroOrOne()
047          .build())
048      .focusDependent()
049      .contextDependent()
050      .deterministic()
051      .returnZeroOrOne()
052      .returnOne()
053      .functionHandler(ResolveReference::executeOneArg)
054      .build();
055
056  @NonNull
057  static final IFunction SIGNATURE_TWO_ARGS = IFunction.builder()
058      .name(NAME)
059      .namespace(OscalModelConstants.NS_OSCAL)
060      .argument(IArgument.builder()
061          .name("uri")
062          .type(IAnyUriItem.type())
063          .zeroOrOne()
064          .build())
065      .argument(IArgument.builder()
066          .name("mediaType")
067          .type(IStringItem.type())
068          .zeroOrOne()
069          .build())
070      .focusIndependent()
071      .contextDependent()
072      .deterministic()
073      .returnType(IAnyUriItem.type())
074      .returnZeroOrOne()
075      .functionHandler(ResolveReference::executeTwoArg)
076      .build();
077
078  private ResolveReference() {
079    // disable construction
080  }
081
082  @SuppressWarnings({ "unused",
083      "PMD.OnlyOneReturn" // readability
084  })
085  @NonNull
086  private static ISequence<?> executeOneArg(
087      @NonNull IFunction function,
088      @NonNull List<ISequence<?>> arguments,
089      @NonNull DynamicContext dynamicContext,
090      IItem focus) {
091    IAnyUriItem uri = FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true));
092
093    if (uri == null) {
094      return ISequence.empty();
095    }
096
097    INodeItem node = checkForNodeItem(focus);
098    return ISequence.of(resolveReference(uri, null, node));
099  }
100
101  @SuppressWarnings({ "unused",
102      "PMD.OnlyOneReturn" // readability
103  })
104  @NonNull
105  private static ISequence<?> executeTwoArg(
106      @NonNull IFunction function,
107      @NonNull List<ISequence<?>> arguments,
108      @NonNull DynamicContext dynamicContext,
109      IItem focus) {
110    IAnyUriItem uri = FunctionUtils.asTypeOrNull(ObjectUtils.requireNonNull(arguments.get(0).getFirstItem(true)));
111
112    if (uri == null) {
113      return ISequence.empty();
114    }
115
116    // this function is focus dependent, so the focus must be non null
117    assert focus != null;
118
119    IStringItem mediaType = FunctionUtils.asTypeOrNull(ObjectUtils.requireNonNull(arguments.get(1).getFirstItem(true)));
120    INodeItem node = checkForNodeItem(focus);
121    return ISequence.of(resolveReference(uri, mediaType, node));
122  }
123
124  /**
125   * Ensure the provided item is a node item.
126   *
127   * @param item
128   *          the item to check
129   * @return the item as a node item
130   * @throws InvalidArgumentFunctionException
131   *           with code
132   *           {@link InvalidArgumentFunctionException#INVALID_ARGUMENT_TYPE} if
133   *           the item is not a node item
134   */
135  private static INodeItem checkForNodeItem(@NonNull IItem item) {
136    if (!(item instanceof INodeItem)) {
137      // this is expected to be a node
138      throw new InvalidArgumentFunctionException(
139          InvalidArgumentFunctionException.INVALID_ARGUMENT_TYPE,
140          String.format("Item of type '%s' is not a node item.", item.getClass().getName()));
141    }
142    return (INodeItem) item;
143  }
144
145  @NonNull
146  public static IAnyUriItem resolveReference(
147      @NonNull IAnyUriItem reference,
148      @Nullable IStringItem mediaType,
149      @NonNull INodeItem focusedItem) {
150    INodeItem root = FnRoot.fnRoot(focusedItem);
151    IOscalInstance oscalInstance = (IOscalInstance) INodeItem.toValue(root);
152
153    URI referenceUri = reference.asUri();
154    String fragment = referenceUri.getFragment();
155
156    return fragment != null
157        && (referenceUri.getPath() == null || referenceUri.getPath().isEmpty())
158            ? IAnyUriItem.valueOf(resolveReference(
159                fragment,
160                mediaType == null ? null : mediaType.asString(),
161                oscalInstance))
162            : reference;
163  }
164
165  @NonNull
166  public static URI resolveReference(
167      @NonNull String reference,
168      @Nullable String mediaType,
169      @NonNull IOscalInstance oscalInstance) {
170    Resource resource = oscalInstance.getResourceByUuid(IUuidItem.valueOf(reference).asUuid());
171    if (resource == null) {
172      throw new UnidentifiedFunctionError(
173          String.format("A backmatter resource with the id '%s' does not exist.", reference));
174    }
175
176    Rlink rLink = OscalUtils.findMatchingRLink(resource, mediaType);
177    if (rLink == null) {
178      throw new UnidentifiedFunctionError(
179          String.format("The backmatter resource '%s' does not have an rlink entry.", reference));
180    }
181    return rLink.getHref();
182  }
183}