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.context;
021
022import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
023import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
024import jakarta.annotation.Nonnull;
025import jakarta.annotation.Nullable;
026import org.apache.commons.lang3.builder.EqualsBuilder;
027import org.apache.commons.lang3.builder.HashCodeBuilder;
028import org.apache.commons.lang3.builder.ToStringBuilder;
029import org.apache.commons.lang3.builder.ToStringStyle;
030import org.hl7.fhir.instance.model.api.IBaseExtension;
031import org.hl7.fhir.instance.model.api.IIdType;
032
033import java.util.ArrayList;
034import java.util.Collection;
035import java.util.Collections;
036import java.util.HashMap;
037import java.util.HashSet;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041import java.util.StringTokenizer;
042
043import static org.apache.commons.lang3.StringUtils.isNotBlank;
044import static org.apache.commons.lang3.StringUtils.trim;
045
046public class RuntimeSearchParam {
047        private final IIdType myId;
048        private final Set<String> myBase;
049        private final String myDescription;
050        private final String myName;
051        private final RestSearchParameterTypeEnum myParamType;
052        private final String myPath;
053        private final Set<String> myTargets;
054        private final Set<String> myProvidesMembershipInCompartments;
055        private final RuntimeSearchParamStatusEnum myStatus;
056        private final String myUri;
057        private final Map<String, List<IBaseExtension<?, ?>>> myExtensions = new HashMap<>();
058        private final Map<String, String> myUpliftRefchains = new HashMap<>();
059        private final ComboSearchParamType myComboSearchParamType;
060        private final List<Component> myComponents;
061        private IPhoneticEncoder myPhoneticEncoder;
062
063        /**
064         * Constructor
065         */
066        public RuntimeSearchParam(
067                        IIdType theId,
068                        String theUri,
069                        String theName,
070                        String theDescription,
071                        String thePath,
072                        RestSearchParameterTypeEnum theParamType,
073                        Set<String> theProvidesMembershipInCompartments,
074                        Set<String> theTargets,
075                        RuntimeSearchParamStatusEnum theStatus,
076                        Collection<String> theBase) {
077                this(
078                                theId,
079                                theUri,
080                                theName,
081                                theDescription,
082                                thePath,
083                                theParamType,
084                                theProvidesMembershipInCompartments,
085                                theTargets,
086                                theStatus,
087                                null,
088                                Collections.emptyList(),
089                                theBase);
090        }
091
092        /**
093         * Copy constructor
094         */
095        public RuntimeSearchParam(RuntimeSearchParam theSp) {
096                this(
097                                theSp.getId(),
098                                theSp.getUri(),
099                                theSp.getName(),
100                                theSp.getDescription(),
101                                theSp.getPath(),
102                                theSp.getParamType(),
103                                theSp.getProvidesMembershipInCompartments(),
104                                theSp.getTargets(),
105                                theSp.getStatus(),
106                                theSp.getComboSearchParamType(),
107                                theSp.getComponents(),
108                                theSp.getBase());
109        }
110
111        /**
112         * Constructor
113         */
114        public RuntimeSearchParam(
115                        IIdType theId,
116                        String theUri,
117                        String theName,
118                        String theDescription,
119                        String thePath,
120                        RestSearchParameterTypeEnum theParamType,
121                        Set<String> theProvidesMembershipInCompartments,
122                        Set<String> theTargets,
123                        RuntimeSearchParamStatusEnum theStatus,
124                        ComboSearchParamType theComboSearchParamType,
125                        List<Component> theComponents,
126                        Collection<String> theBase) {
127                super();
128
129                myId = theId;
130                myUri = theUri;
131                myName = theName;
132                myDescription = theDescription;
133                myPath = thePath;
134                myParamType = theParamType;
135                myStatus = theStatus;
136                if (theProvidesMembershipInCompartments != null && !theProvidesMembershipInCompartments.isEmpty()) {
137                        myProvidesMembershipInCompartments = Collections.unmodifiableSet(theProvidesMembershipInCompartments);
138                } else {
139                        myProvidesMembershipInCompartments = null;
140                }
141                if (theTargets != null && theTargets.isEmpty() == false) {
142                        myTargets = Collections.unmodifiableSet(theTargets);
143                } else {
144                        myTargets = Collections.emptySet();
145                }
146
147                if (theBase == null || theBase.isEmpty()) {
148                        HashSet<String> base = new HashSet<>();
149                        if (isNotBlank(thePath)) {
150                                int indexOf = thePath.indexOf('.');
151                                if (indexOf != -1) {
152                                        base.add(trim(thePath.substring(0, indexOf)));
153                                }
154                        }
155                        myBase = Collections.unmodifiableSet(base);
156                } else {
157                        myBase = Collections.unmodifiableSet(new HashSet<>(theBase));
158                }
159                myComboSearchParamType = theComboSearchParamType;
160                if (theComponents != null) {
161                        myComponents = Collections.unmodifiableList(theComponents);
162                } else {
163                        myComponents = Collections.emptyList();
164                }
165        }
166
167        public List<Component> getComponents() {
168                return myComponents;
169        }
170
171        /**
172         * Returns <code>null</code> if this is not a combo search param type
173         */
174        @Nullable
175        public ComboSearchParamType getComboSearchParamType() {
176                return myComboSearchParamType;
177        }
178
179        /**
180         * Retrieve user data - This can be used to store any application-specific data
181         */
182        @Nonnull
183        public List<IBaseExtension<?, ?>> getExtensions(String theKey) {
184                List<IBaseExtension<?, ?>> retVal = myExtensions.get(theKey);
185                if (retVal != null) {
186                        retVal = Collections.unmodifiableList(retVal);
187                } else {
188                        retVal = Collections.emptyList();
189                }
190                return retVal;
191        }
192
193        /**
194         * Sets user data - This can be used to store any application-specific data
195         */
196        public RuntimeSearchParam addExtension(String theKey, IBaseExtension<?, ?> theValue) {
197                List<IBaseExtension<?, ?>> valuesList = myExtensions.computeIfAbsent(theKey, k -> new ArrayList<>());
198                valuesList.add(theValue);
199                return this;
200        }
201
202        @Override
203        public String toString() {
204                return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
205                                .append("base", myBase)
206                                .append("name", myName)
207                                .append("path", myPath)
208                                .append("id", myId)
209                                .append("uri", myUri)
210                                .toString();
211        }
212
213        public IIdType getId() {
214                return myId;
215        }
216
217        public String getUri() {
218                return myUri;
219        }
220
221        @Override
222        public boolean equals(Object theO) {
223                if (this == theO) return true;
224
225                if (theO == null || getClass() != theO.getClass()) return false;
226
227                RuntimeSearchParam that = (RuntimeSearchParam) theO;
228
229                return new EqualsBuilder()
230                                .append(getId(), that.getId())
231                                .append(getName(), that.getName())
232                                .append(getPath(), that.getPath())
233                                .append(getUri(), that.getUri())
234                                .isEquals();
235        }
236
237        @Override
238        public int hashCode() {
239                return new HashCodeBuilder(17, 37)
240                                .append(getId())
241                                .append(getName())
242                                .append(getPath())
243                                .append(getUri())
244                                .toHashCode();
245        }
246
247        public Set<String> getBase() {
248                return myBase;
249        }
250
251        @Nonnull
252        public Set<String> getTargets() {
253                return myTargets;
254        }
255
256        public boolean hasTargets() {
257                return !myTargets.isEmpty();
258        }
259
260        public RuntimeSearchParamStatusEnum getStatus() {
261                return myStatus;
262        }
263
264        public String getDescription() {
265                return myDescription;
266        }
267
268        public String getName() {
269                return myName;
270        }
271
272        public RestSearchParameterTypeEnum getParamType() {
273                return myParamType;
274        }
275
276        public String getPath() {
277                return myPath;
278        }
279
280        public List<String> getPathsSplit() {
281                return getPathsSplitForResourceType(null);
282        }
283
284        /**
285         * Can return null
286         */
287        public Set<String> getProvidesMembershipInCompartments() {
288                return myProvidesMembershipInCompartments;
289        }
290
291        public RuntimeSearchParam setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) {
292                myPhoneticEncoder = thePhoneticEncoder;
293                return this;
294        }
295
296        public String encode(String theString) {
297                if (myPhoneticEncoder == null || theString == null) {
298                        return theString;
299                }
300                return myPhoneticEncoder.encode(theString);
301        }
302
303        public List<String> getPathsSplitForResourceType(@Nullable String theResourceName) {
304                String path = getPath();
305                if (path.indexOf('|') == -1) {
306                        if (theResourceName != null && !pathMatchesResourceType(theResourceName, path)) {
307                                return Collections.emptyList();
308                        }
309                        return Collections.singletonList(path);
310                }
311
312                List<String> retVal = new ArrayList<>();
313                StringTokenizer tok = new StringTokenizer(path, "|");
314                while (tok.hasMoreElements()) {
315                        String nextPath = tok.nextToken().trim();
316                        if (theResourceName != null && !pathMatchesResourceType(theResourceName, nextPath)) {
317                                continue;
318                        }
319                        retVal.add(nextPath.trim());
320                }
321                return retVal;
322        }
323
324        public void addUpliftRefchain(@Nonnull String theCode, @Nonnull String theElementName) {
325                myUpliftRefchains.put(theCode, theElementName);
326        }
327
328        /**
329         * Does this search parameter have an uplift refchain definition for the given code?
330         * See the HAPI FHIR documentation for a description of how uplift refchains work.
331         *
332         * @since 6.6.0
333         */
334        public boolean hasUpliftRefchain(String theCode) {
335                return myUpliftRefchains.containsKey(theCode);
336        }
337
338        /**
339         * Returns a set of all codes associated with uplift refchains for this search parameter.
340         * See the HAPI FHIR documentation for a description of how uplift refchains work.
341         *
342         * @since 6.6.0
343         */
344        public Set<String> getUpliftRefchainCodes() {
345                return Collections.unmodifiableSet(myUpliftRefchains.keySet());
346        }
347
348        /**
349         * Does this search parameter have any uplift refchain definitions?
350         * See the HAPI FHIR documentation for a description of how uplift refchains work.
351         *
352         * @since 6.6.0
353         */
354        public boolean hasUpliftRefchains() {
355                return !myUpliftRefchains.isEmpty();
356        }
357
358        public enum RuntimeSearchParamStatusEnum {
359                ACTIVE,
360                DRAFT,
361                RETIRED,
362                UNKNOWN
363        }
364
365        /**
366         * This method tests whether a given FHIRPath expression <i>could</i>
367         * possibly apply to the given resource type.
368         *
369         * @param theResourceName
370         * @param thePath
371         * @return
372         */
373        static boolean pathMatchesResourceType(String theResourceName, String thePath) {
374                for (int i = 0; i < thePath.length() - 1; i++) {
375                        char nextChar = thePath.charAt(i);
376                        if (Character.isLowerCase(nextChar)) {
377                                return true;
378                        }
379                        if (Character.isLetter(nextChar)) {
380                                if (fhirPathExpressionStartsWith(theResourceName, thePath, i)) {
381                                        return true;
382                                }
383                                if (fhirPathExpressionStartsWith("Resource", thePath, i)) {
384                                        return true;
385                                }
386                                if (fhirPathExpressionStartsWith("DomainResource", thePath, i)) {
387                                        return true;
388                                }
389                                return false;
390                        }
391                }
392
393                return false;
394        }
395
396        private static boolean fhirPathExpressionStartsWith(String theResourceName, String thePath, int theStartingIndex) {
397                if (thePath.startsWith(theResourceName, theStartingIndex) && thePath.length() > theResourceName.length()) {
398                        for (int i = theResourceName.length() + theStartingIndex; i < thePath.length(); i++) {
399                                char nextChar = thePath.charAt(i);
400                                if (nextChar == '.') {
401                                        return true;
402                                } else if (nextChar != ' ') {
403                                        return false;
404                                }
405                        }
406                }
407                return false;
408        }
409
410        public static class Component {
411                private final String myExpression;
412                private final String myReference;
413
414                /**
415                 * Constructor
416                 */
417                public Component(String theExpression, String theReference) {
418                        myExpression = theExpression;
419                        myReference = theReference;
420                }
421
422                @Override
423                public String toString() {
424                        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
425                                        .append("expression", myExpression)
426                                        .append("reference", myReference)
427                                        .toString();
428                }
429
430                public String getExpression() {
431                        return myExpression;
432                }
433
434                public String getReference() {
435                        return myReference;
436                }
437        }
438}