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.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 javax.annotation.Nonnull;
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(FhirContext theFhirContext, IBase theBase, String theUrl, String theValueType, Object theValue) {
107                IBaseExtension<?,?> ext = addExtension(theBase, theUrl);
108                setExtension(theFhirContext, ext, theValueType, theValue);
109        }
110
111        private static IBaseHasExtensions validateExtensionSupport(IBase theBase) {
112                if (!(theBase instanceof IBaseHasExtensions)) {
113                        throw new IllegalArgumentException(Msg.code(1747) + String.format("Expected instance that supports extensions, but got %s", theBase));
114                }
115                return (IBaseHasExtensions) theBase;
116        }
117
118        /**
119         * Checks if the specified instance has an extension with the specified URL
120         *
121         * @param theBase         The base resource to check extensions on
122         * @param theExtensionUrl URL of the extension
123         * @return Returns true if extension is exists and false otherwise
124         */
125        public static boolean hasExtension(IBase theBase, String theExtensionUrl) {
126                IBaseHasExtensions baseHasExtensions;
127                try {
128                        baseHasExtensions = validateExtensionSupport(theBase);
129                } catch (Exception e) {
130                        return false;
131                }
132
133                return getExtensionByUrl(baseHasExtensions, theExtensionUrl) != null;
134        }
135
136        /**
137         * Checks if the specified instance has an extension with the specified URL
138         *
139         * @param theBase         The base resource to check extensions on
140         * @param theExtensionUrl URL of the extension
141         * @return Returns true if extension is exists and false otherwise
142         */
143        public static boolean hasExtension(IBase theBase, String theExtensionUrl, String theExtensionValue) {
144                if (!hasExtension(theBase, theExtensionUrl)) {
145                        return false;
146                }
147                IBaseDatatype value = getExtensionByUrl(theBase, theExtensionUrl).getValue();
148                if (value == null) {
149                        return theExtensionValue == null;
150                }
151                return value.toString().equals(theExtensionValue);
152        }
153
154        /**
155         * Gets the first extension with the specified URL
156         *
157         * @param theBase         The resource to get the extension for
158         * @param theExtensionUrl URL of the extension to get. Must be non-null
159         * @return Returns the first available extension with the specified URL, or null if such extension doesn't exist
160         */
161        public static IBaseExtension<?, ?> getExtensionByUrl(IBase theBase, String theExtensionUrl) {
162                Predicate<IBaseExtension<?,?>> filter;
163                if (theExtensionUrl == null) {
164                        filter = (e -> true);
165                } else {
166                        filter = (e -> theExtensionUrl.equals(e.getUrl()));
167                }
168
169                return getExtensionsMatchingPredicate(theBase, filter)
170                        .stream()
171                        .findFirst()
172                        .orElse(null);
173        }
174
175        /**
176         * Given a resource or other structure that can have direct extensions,
177         * pulls out any extensions that have the given theExtensionUrl and a primitive value type,
178         * and returns a list of the string version of the extension values.
179         */
180        public static List<String> getExtensionPrimitiveValues(IBaseHasExtensions theBase, String theExtensionUrl) {
181                List<String> values = theBase
182                        .getExtension()
183                        .stream()
184                        .filter(t -> theExtensionUrl.equals(t.getUrl()))
185                        .filter(t -> t.getValue() instanceof IPrimitiveType<?>)
186                        .map(t->(IPrimitiveType<?>)t.getValue())
187                        .map(IPrimitiveType::getValueAsString)
188                        .filter(StringUtils::isNotBlank)
189                        .collect(Collectors.toList());
190                return values;
191        }
192
193
194        /**
195         * Gets all extensions that match the specified filter predicate
196         *
197         * @param theBase   The resource to get the extension for
198         * @param theFilter Predicate to match the extension against
199         * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist
200         */
201        public static List<IBaseExtension<?, ?>> getExtensionsMatchingPredicate(IBase theBase, Predicate<? super IBaseExtension<?,?>> theFilter) {
202                return validateExtensionSupport(theBase)
203                        .getExtension()
204                        .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(IBase theBase, Predicate<? super IBaseExtension<?,?>> theFilter) {
238                List<IBaseExtension<?, ?>> retVal = getExtensionsMatchingPredicate(theBase, theFilter);
239                validateExtensionSupport(theBase)
240                        .getExtension()
241                        .removeIf(theFilter);
242                return retVal;
243        }
244
245        /**
246         * Gets all extensions with the specified URL
247         *
248         * @param theBase         The resource to get the extension for
249         * @param theExtensionUrl URL of the extension to get. Must be non-null
250         * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist
251         */
252        public static List<IBaseExtension<?, ?>> getExtensionsByUrl(IBaseHasExtensions theBase, String theExtensionUrl) {
253                Predicate<IBaseExtension<?,?>> urlEqualityPredicate = e -> theExtensionUrl.equals(e.getUrl());
254                return getExtensionsMatchingPredicate(theBase, urlEqualityPredicate);
255        }
256
257        /**
258         * Sets value of the extension as a string
259         *
260         * @param theExtension   The extension to set the value on
261         * @param theValue       The value to set
262         * @param theFhirContext The context containing FHIR resource definitions
263         */
264        public static void setExtension(FhirContext theFhirContext, IBaseExtension<?,?> theExtension, String theValue) {
265                setExtension(theFhirContext, theExtension, "string", theValue);
266        }
267
268        /**
269         * Sets value of the extension
270         *
271         * @param theExtension     The extension to set the value on
272         * @param theExtensionType Element type of the extension
273         * @param theValue         The value to set
274         * @param theFhirContext   The context containing FHIR resource definitions
275         */
276        public static void setExtension(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(FhirContext theFhirContext, IBase theBase, String theUrl, String theValueType, Object theValue) {
303                IBaseExtension<?,?> ext = getOrCreateExtension(theBase, theUrl);
304                setExtension(theFhirContext, ext, theValueType, theValue);
305        }
306
307        /**
308         * Compares two extensions, returns true if they have the same value and url
309         *
310         * @param theLeftExtension  : Extension to be evaluated #1
311         * @param theRightExtension : Extension to be evaluated #2
312         * @return Result of the comparison
313         */
314        public static boolean equals(IBaseExtension<?,?> theLeftExtension, IBaseExtension<?,?> theRightExtension) {
315                return TerserUtil.equals(theLeftExtension, theRightExtension);
316        }
317
318        /**
319         * Given an extension, looks for the first child extension with the given URL of {@literal theChildExtensionUrl}
320         * and a primitive datatype value, and returns the String version of that value. E.g. if the
321         * value is a FHIR boolean, it would return the string "true" or "false. If the extension
322         * has no value, or the value is not a primitive datatype, or the URL is not found, the method
323         * will return {@literal null}.
324         *
325         * @param theExtension The parent extension. Must not be null.
326         * @param theChildExtensionUrl The child extension URL. Must not be null or blank.
327         * @since 6.6.0
328         */
329        public static <D, T extends IBaseExtension<T, D>> String extractChildPrimitiveExtensionValue(@Nonnull IBaseExtension<T, D> theExtension, @Nonnull String theChildExtensionUrl) {
330                Validate.notNull(theExtension, "theExtension must not be null");
331                Validate.notBlank(theChildExtensionUrl, "theChildExtensionUrl must not be null or blank");
332
333                Optional<T> codeExtension = theExtension
334                        .getExtension()
335                        .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 = (IPrimitiveType<?>) codeExtension.get().getValue();
341                        retVal = codeValue.getValueAsString();
342                }
343                return retVal;
344        }
345}