1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.oscal.lib.metapath.function.library;
7   
8   import java.net.URI;
9   import java.util.List;
10  
11  import dev.metaschema.core.metapath.DynamicContext;
12  import dev.metaschema.core.metapath.function.FunctionUtils;
13  import dev.metaschema.core.metapath.function.IArgument;
14  import dev.metaschema.core.metapath.function.IFunction;
15  import dev.metaschema.core.metapath.function.InvalidArgumentFunctionException;
16  import dev.metaschema.core.metapath.function.UnidentifiedFunctionError;
17  import dev.metaschema.core.metapath.function.library.FnRoot;
18  import dev.metaschema.core.metapath.item.IItem;
19  import dev.metaschema.core.metapath.item.ISequence;
20  import dev.metaschema.core.metapath.item.atomic.IAnyUriItem;
21  import dev.metaschema.core.metapath.item.atomic.IStringItem;
22  import dev.metaschema.core.metapath.item.atomic.IUuidItem;
23  import dev.metaschema.core.metapath.item.node.INodeItem;
24  import dev.metaschema.core.util.ObjectUtils;
25  import dev.metaschema.oscal.lib.OscalModelConstants;
26  import dev.metaschema.oscal.lib.OscalUtils;
27  import dev.metaschema.oscal.lib.model.BackMatter.Resource;
28  import dev.metaschema.oscal.lib.model.BackMatter.Resource.Rlink;
29  import dev.metaschema.oscal.lib.model.IOscalInstance;
30  import edu.umd.cs.findbugs.annotations.NonNull;
31  import edu.umd.cs.findbugs.annotations.Nullable;
32  
33  /**
34   * Supports resolving a link to a backmatter resource.
35   */
36  public final class ResolveReference {
37    private static final String NAME = "resolve-reference";
38  
39    @NonNull
40    static final IFunction SIGNATURE_ONE_ARG = IFunction.builder()
41        .name(NAME)
42        .namespace(OscalModelConstants.NS_OSCAL)
43        .argument(IArgument.builder()
44            .name("uri")
45            .type(IAnyUriItem.type())
46            .zeroOrOne()
47            .build())
48        .focusDependent()
49        .contextDependent()
50        .deterministic()
51        .returnZeroOrOne()
52        .returnOne()
53        .functionHandler(ResolveReference::executeOneArg)
54        .build();
55  
56    @NonNull
57    static final IFunction SIGNATURE_TWO_ARGS = IFunction.builder()
58        .name(NAME)
59        .namespace(OscalModelConstants.NS_OSCAL)
60        .argument(IArgument.builder()
61            .name("uri")
62            .type(IAnyUriItem.type())
63            .zeroOrOne()
64            .build())
65        .argument(IArgument.builder()
66            .name("mediaType")
67            .type(IStringItem.type())
68            .zeroOrOne()
69            .build())
70        .focusIndependent()
71        .contextDependent()
72        .deterministic()
73        .returnType(IAnyUriItem.type())
74        .returnZeroOrOne()
75        .functionHandler(ResolveReference::executeTwoArg)
76        .build();
77  
78    private ResolveReference() {
79      // disable construction
80    }
81  
82    @SuppressWarnings({ "unused",
83        "PMD.OnlyOneReturn" // readability
84    })
85    @NonNull
86    private static ISequence<?> executeOneArg(
87        @NonNull IFunction function,
88        @NonNull List<ISequence<?>> arguments,
89        @NonNull DynamicContext dynamicContext,
90        IItem focus) {
91      IAnyUriItem uri = FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true));
92  
93      if (uri == null) {
94        return ISequence.empty();
95      }
96  
97      INodeItem node = checkForNodeItem(focus);
98      return ISequence.of(resolveReference(uri, null, node));
99    }
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 }