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}