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}