001/*- 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.util; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import jakarta.annotation.Nonnull; 025import org.apache.commons.lang3.StringUtils; 026import org.apache.commons.lang3.Validate; 027import org.hl7.fhir.instance.model.api.IBase; 028import org.hl7.fhir.instance.model.api.IBaseDatatype; 029import org.hl7.fhir.instance.model.api.IBaseExtension; 030import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 031import org.hl7.fhir.instance.model.api.IPrimitiveType; 032 033import java.util.Collections; 034import java.util.List; 035import java.util.Optional; 036import java.util.function.Predicate; 037import java.util.stream.Collectors; 038 039/** 040 * Utility for modifying with extensions in a FHIR version-independent approach. 041 */ 042public class ExtensionUtil { 043 044 /** 045 * Non instantiable 046 */ 047 private ExtensionUtil() { 048 // nothing 049 } 050 051 /** 052 * Returns an extension with the specified URL creating one if it doesn't exist. 053 * 054 * @param theBase Base resource to get extension from 055 * @param theUrl URL for the extension 056 * @return Returns a extension with the specified URL. 057 * @throws IllegalArgumentException IllegalArgumentException is thrown in case resource doesn't support extensions 058 */ 059 public static IBaseExtension<?, ?> getOrCreateExtension(IBase theBase, String theUrl) { 060 IBaseHasExtensions baseHasExtensions = validateExtensionSupport(theBase); 061 IBaseExtension<?, ?> extension = getExtensionByUrl(baseHasExtensions, theUrl); 062 if (extension == null) { 063 extension = baseHasExtensions.addExtension(); 064 extension.setUrl(theUrl); 065 } 066 return extension; 067 } 068 069 /** 070 * Returns an new empty extension. 071 * 072 * @param theBase Base resource to add the extension to 073 * @return Returns a new extension 074 * @throws IllegalArgumentException IllegalArgumentException is thrown in case resource doesn't support extensions 075 */ 076 public static IBaseExtension<?, ?> addExtension(IBase theBase) { 077 return addExtension(theBase, null); 078 } 079 080 /** 081 * Returns an extension with the specified URL 082 * 083 * @param theBase Base resource to add the extension to 084 * @param theUrl URL for the extension 085 * @return Returns a new extension with the specified URL. 086 * @throws IllegalArgumentException IllegalArgumentException is thrown in case resource doesn't support extensions 087 */ 088 public static IBaseExtension<?, ?> addExtension(IBase theBase, String theUrl) { 089 IBaseHasExtensions baseHasExtensions = validateExtensionSupport(theBase); 090 IBaseExtension<?, ?> extension = baseHasExtensions.addExtension(); 091 if (theUrl != null) { 092 extension.setUrl(theUrl); 093 } 094 return extension; 095 } 096 097 /** 098 * Adds an extension with the specified value 099 * 100 * @param theBase The resource to update extension on 101 * @param theUrl Extension URL 102 * @param theValueType Type of the value to set in the extension 103 * @param theValue Extension value 104 * @param theFhirContext The context containing FHIR resource definitions 105 */ 106 public static void addExtension( 107 FhirContext theFhirContext, IBase theBase, String theUrl, String theValueType, Object theValue) { 108 IBaseExtension<?, ?> ext = addExtension(theBase, theUrl); 109 setExtension(theFhirContext, ext, theValueType, theValue); 110 } 111 112 private static IBaseHasExtensions validateExtensionSupport(IBase theBase) { 113 if (!(theBase instanceof IBaseHasExtensions)) { 114 throw new IllegalArgumentException( 115 Msg.code(1747) + String.format("Expected instance that supports extensions, but got %s", theBase)); 116 } 117 return (IBaseHasExtensions) theBase; 118 } 119 120 /** 121 * Checks if the specified instance has an extension with the specified URL 122 * 123 * @param theBase The base resource to check extensions on 124 * @param theExtensionUrl URL of the extension 125 * @return Returns true if extension is exists and false otherwise 126 */ 127 public static boolean hasExtension(IBase theBase, String theExtensionUrl) { 128 IBaseHasExtensions baseHasExtensions; 129 try { 130 baseHasExtensions = validateExtensionSupport(theBase); 131 } catch (Exception e) { 132 return false; 133 } 134 135 return getExtensionByUrl(baseHasExtensions, theExtensionUrl) != null; 136 } 137 138 /** 139 * Checks if the specified instance has an extension with the specified URL 140 * 141 * @param theBase The base resource to check extensions on 142 * @param theExtensionUrl URL of the extension 143 * @return Returns true if extension is exists and false otherwise 144 */ 145 public static boolean hasExtension(IBase theBase, String theExtensionUrl, String theExtensionValue) { 146 if (!hasExtension(theBase, theExtensionUrl)) { 147 return false; 148 } 149 IBaseDatatype value = getExtensionByUrl(theBase, theExtensionUrl).getValue(); 150 if (value == null) { 151 return theExtensionValue == null; 152 } 153 return value.toString().equals(theExtensionValue); 154 } 155 156 /** 157 * Gets the first extension with the specified URL 158 * 159 * @param theBase The resource to get the extension for 160 * @param theExtensionUrl URL of the extension to get. Must be non-null 161 * @return Returns the first available extension with the specified URL, or null if such extension doesn't exist 162 */ 163 public static IBaseExtension<?, ?> getExtensionByUrl(IBase theBase, String theExtensionUrl) { 164 Predicate<IBaseExtension<?, ?>> filter; 165 if (theExtensionUrl == null) { 166 filter = (e -> true); 167 } else { 168 filter = (e -> theExtensionUrl.equals(e.getUrl())); 169 } 170 171 return getExtensionsMatchingPredicate(theBase, filter).stream() 172 .findFirst() 173 .orElse(null); 174 } 175 176 /** 177 * Given a resource or other structure that can have direct extensions, 178 * pulls out any extensions that have the given theExtensionUrl and a primitive value type, 179 * and returns a list of the string version of the extension values. 180 */ 181 public static List<String> getExtensionPrimitiveValues(IBase theBase, String theExtensionUrl) { 182 if (theBase instanceof IBaseHasExtensions) { 183 return ((IBaseHasExtensions) theBase) 184 .getExtension().stream() 185 .filter(t -> theExtensionUrl.equals(t.getUrl())) 186 .filter(t -> t.getValue() instanceof IPrimitiveType<?>) 187 .map(t -> (IPrimitiveType<?>) t.getValue()) 188 .map(IPrimitiveType::getValueAsString) 189 .filter(StringUtils::isNotBlank) 190 .collect(Collectors.toList()); 191 } 192 return Collections.emptyList(); 193 } 194 195 /** 196 * Gets all extensions that match the specified filter predicate 197 * 198 * @param theBase The resource to get the extension for 199 * @param theFilter Predicate to match the extension against 200 * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist 201 */ 202 public static List<IBaseExtension<?, ?>> getExtensionsMatchingPredicate( 203 IBase theBase, Predicate<? super IBaseExtension<?, ?>> theFilter) { 204 return validateExtensionSupport(theBase).getExtension().stream() 205 .filter(theFilter) 206 .collect(Collectors.toList()); 207 } 208 209 /** 210 * Removes all extensions. 211 * 212 * @param theBase The resource to clear the extension for 213 * @return Returns all extension that were removed 214 */ 215 public static List<IBaseExtension<?, ?>> clearAllExtensions(IBase theBase) { 216 return clearExtensionsMatchingPredicate(theBase, (e -> true)); 217 } 218 219 /** 220 * Removes all extensions by URL. 221 * 222 * @param theBase The resource to clear the extension for 223 * @param theUrl The url to clear extensions for 224 * @return Returns all extension that were removed 225 */ 226 public static List<IBaseExtension<?, ?>> clearExtensionsByUrl(IBase theBase, String theUrl) { 227 return clearExtensionsMatchingPredicate(theBase, (e -> theUrl.equals(e.getUrl()))); 228 } 229 230 /** 231 * Removes all extensions that match the specified predicate 232 * 233 * @param theBase The base object to clear the extension for 234 * @param theFilter Defines which extensions should be cleared 235 * @return Returns all extension that were removed 236 */ 237 private static List<IBaseExtension<?, ?>> clearExtensionsMatchingPredicate( 238 IBase theBase, Predicate<? super IBaseExtension<?, ?>> theFilter) { 239 List<IBaseExtension<?, ?>> retVal = getExtensionsMatchingPredicate(theBase, theFilter); 240 validateExtensionSupport(theBase).getExtension().removeIf(theFilter); 241 return retVal; 242 } 243 244 /** 245 * Gets all extensions with the specified URL 246 * 247 * @param theBase The resource to get the extension for 248 * @param theExtensionUrl URL of the extension to get. Must be non-null 249 * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist 250 */ 251 public static List<IBaseExtension<?, ?>> getExtensionsByUrl(IBaseHasExtensions theBase, String theExtensionUrl) { 252 Predicate<IBaseExtension<?, ?>> urlEqualityPredicate = e -> theExtensionUrl.equals(e.getUrl()); 253 return getExtensionsMatchingPredicate(theBase, urlEqualityPredicate); 254 } 255 256 /** 257 * Sets value of the extension as a string 258 * 259 * @param theExtension The extension to set the value on 260 * @param theValue The value to set 261 * @param theFhirContext The context containing FHIR resource definitions 262 */ 263 public static void setExtension(FhirContext theFhirContext, IBaseExtension<?, ?> theExtension, String theValue) { 264 setExtension(theFhirContext, theExtension, "string", theValue); 265 } 266 267 /** 268 * Sets value of the extension 269 * 270 * @param theExtension The extension to set the value on 271 * @param theExtensionType Element type of the extension 272 * @param theValue The value to set 273 * @param theFhirContext The context containing FHIR resource definitions 274 */ 275 public static void setExtension( 276 FhirContext theFhirContext, IBaseExtension<?, ?> theExtension, String theExtensionType, Object theValue) { 277 theExtension.setValue(TerserUtil.newElement(theFhirContext, theExtensionType, theValue)); 278 } 279 280 /** 281 * Sets or replaces existing extension with the specified value as a string 282 * 283 * @param theBase The resource to update extension on 284 * @param theUrl Extension URL 285 * @param theValue Extension value 286 * @param theFhirContext The context containing FHIR resource definitions 287 */ 288 public static void setExtensionAsString(FhirContext theFhirContext, IBase theBase, String theUrl, String theValue) { 289 IBaseExtension<?, ?> ext = getOrCreateExtension(theBase, theUrl); 290 setExtension(theFhirContext, ext, theValue); 291 } 292 293 /** 294 * Sets or replaces existing extension with the specified value 295 * 296 * @param theBase The resource to update extension on 297 * @param theUrl Extension URL 298 * @param theValueType Type of the value to set in the extension 299 * @param theValue Extension value 300 * @param theFhirContext The context containing FHIR resource definitions 301 */ 302 public static void setExtension( 303 FhirContext theFhirContext, IBase theBase, String theUrl, String theValueType, Object theValue) { 304 IBaseExtension<?, ?> ext = getOrCreateExtension(theBase, theUrl); 305 setExtension(theFhirContext, ext, theValueType, theValue); 306 } 307 308 /** 309 * Compares two extensions, returns true if they have the same value and url 310 * 311 * @param theLeftExtension : Extension to be evaluated #1 312 * @param theRightExtension : Extension to be evaluated #2 313 * @return Result of the comparison 314 */ 315 public static boolean equals(IBaseExtension<?, ?> theLeftExtension, IBaseExtension<?, ?> theRightExtension) { 316 return TerserUtil.equals(theLeftExtension, theRightExtension); 317 } 318 319 /** 320 * Given an extension, looks for the first child extension with the given URL of {@literal theChildExtensionUrl} 321 * and a primitive datatype value, and returns the String version of that value. E.g. if the 322 * value is a FHIR boolean, it would return the string "true" or "false. If the extension 323 * has no value, or the value is not a primitive datatype, or the URL is not found, the method 324 * will return {@literal null}. 325 * 326 * @param theExtension The parent extension. Must not be null. 327 * @param theChildExtensionUrl The child extension URL. Must not be null or blank. 328 * @since 6.6.0 329 */ 330 public static <D, T extends IBaseExtension<T, D>> String extractChildPrimitiveExtensionValue( 331 @Nonnull IBaseExtension<T, D> theExtension, @Nonnull String theChildExtensionUrl) { 332 Validate.notNull(theExtension, "theExtension must not be null"); 333 Validate.notBlank(theChildExtensionUrl, "theChildExtensionUrl must not be null or blank"); 334 335 Optional<T> codeExtension = theExtension.getExtension().stream() 336 .filter(t -> theChildExtensionUrl.equals(t.getUrl())) 337 .findFirst(); 338 String retVal = null; 339 if (codeExtension.isPresent() && codeExtension.get().getValue() instanceof IPrimitiveType) { 340 IPrimitiveType<?> codeValue = 341 (IPrimitiveType<?>) codeExtension.get().getValue(); 342 retVal = codeValue.getValueAsString(); 343 } 344 return retVal; 345 } 346}