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