001/*-
002 * #%L
003 * HAPI FHIR JPA - Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2025 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.jpa.searchparam.util;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.model.api.IQueryParameterAnd;
027import ca.uhn.fhir.model.api.IQueryParameterType;
028import ca.uhn.fhir.rest.api.QualifiedParamList;
029import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
030import ca.uhn.fhir.rest.param.CompositeAndListParam;
031import ca.uhn.fhir.rest.param.DateAndListParam;
032import ca.uhn.fhir.rest.param.DateParam;
033import ca.uhn.fhir.rest.param.HasAndListParam;
034import ca.uhn.fhir.rest.param.HasParam;
035import ca.uhn.fhir.rest.param.NumberAndListParam;
036import ca.uhn.fhir.rest.param.NumberParam;
037import ca.uhn.fhir.rest.param.QuantityAndListParam;
038import ca.uhn.fhir.rest.param.QuantityParam;
039import ca.uhn.fhir.rest.param.ReferenceAndListParam;
040import ca.uhn.fhir.rest.param.ReferenceParam;
041import ca.uhn.fhir.rest.param.SpecialAndListParam;
042import ca.uhn.fhir.rest.param.SpecialParam;
043import ca.uhn.fhir.rest.param.StringAndListParam;
044import ca.uhn.fhir.rest.param.StringParam;
045import ca.uhn.fhir.rest.param.TokenAndListParam;
046import ca.uhn.fhir.rest.param.TokenParam;
047import ca.uhn.fhir.rest.param.UriAndListParam;
048import ca.uhn.fhir.rest.param.UriParam;
049import ca.uhn.fhir.rest.param.binder.QueryParameterAndBinder;
050import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
051import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
052import jakarta.annotation.Nonnull;
053import jakarta.annotation.Nullable;
054
055import java.util.ArrayList;
056import java.util.Collections;
057import java.util.List;
058
059public enum JpaParamUtil {
060        ;
061
062        /**
063         * This is a utility method intended provided to help the JPA module.
064         */
065        public static IQueryParameterAnd<?> parseQueryParams(
066                        FhirContext theContext,
067                        RestSearchParameterTypeEnum paramType,
068                        String theUnqualifiedParamName,
069                        List<QualifiedParamList> theParameters) {
070                QueryParameterAndBinder binder;
071                switch (paramType) {
072                        case COMPOSITE:
073                                throw new UnsupportedOperationException(Msg.code(496));
074                        case DATE:
075                                binder = new QueryParameterAndBinder(DateAndListParam.class, Collections.emptyList());
076                                break;
077                        case NUMBER:
078                                binder = new QueryParameterAndBinder(NumberAndListParam.class, Collections.emptyList());
079                                break;
080                        case QUANTITY:
081                                binder = new QueryParameterAndBinder(QuantityAndListParam.class, Collections.emptyList());
082                                break;
083                        case REFERENCE:
084                                binder = new QueryParameterAndBinder(ReferenceAndListParam.class, Collections.emptyList());
085                                break;
086                        case STRING:
087                                binder = new QueryParameterAndBinder(StringAndListParam.class, Collections.emptyList());
088                                break;
089                        case TOKEN:
090                                binder = new QueryParameterAndBinder(TokenAndListParam.class, Collections.emptyList());
091                                break;
092                        case URI:
093                                binder = new QueryParameterAndBinder(UriAndListParam.class, Collections.emptyList());
094                                break;
095                        case HAS:
096                                binder = new QueryParameterAndBinder(HasAndListParam.class, Collections.emptyList());
097                                break;
098                        case SPECIAL:
099                                binder = new QueryParameterAndBinder(SpecialAndListParam.class, Collections.emptyList());
100                                break;
101                        default:
102                                throw new IllegalArgumentException(Msg.code(497) + "Parameter '" + theUnqualifiedParamName
103                                                + "' has type " + paramType + " which is currently not supported.");
104                }
105
106                return binder.parse(theContext, theUnqualifiedParamName, theParameters);
107        }
108
109        /**
110         * This is a utility method intended provided to help the JPA module.
111         */
112        public static IQueryParameterAnd<?> parseQueryParams(
113                        ISearchParamRegistry theSearchParamRegistry,
114                        FhirContext theContext,
115                        RuntimeSearchParam theParamDef,
116                        String theUnqualifiedParamName,
117                        List<QualifiedParamList> theParameters) {
118
119                RestSearchParameterTypeEnum paramType = theParamDef.getParamType();
120
121                if (paramType == RestSearchParameterTypeEnum.COMPOSITE) {
122
123                        List<ComponentAndCorrespondingParam> compositeList =
124                                        resolveCompositeComponents(theSearchParamRegistry, theParamDef);
125
126                        if (compositeList.size() != 2) {
127                                throw new ConfigurationException(Msg.code(498) + "Search parameter of type " + theUnqualifiedParamName
128                                                + " must have 2 composite types declared in parameter annotation, found "
129                                                + compositeList.size());
130                        }
131
132                        RuntimeSearchParam left = compositeList.get(0).getComponentParameter();
133                        RuntimeSearchParam right = compositeList.get(1).getComponentParameter();
134
135                        @SuppressWarnings({"unchecked", "rawtypes"})
136                        CompositeAndListParam<IQueryParameterType, IQueryParameterType> cp = new CompositeAndListParam(
137                                        getCompositeBindingClass(left.getParamType(), left.getName()),
138                                        getCompositeBindingClass(right.getParamType(), right.getName()));
139
140                        cp.setValuesAsQueryTokens(theContext, theUnqualifiedParamName, theParameters);
141
142                        return cp;
143                } else {
144                        return parseQueryParams(theContext, paramType, theUnqualifiedParamName, theParameters);
145                }
146        }
147
148        /**
149         * Given a composite or combo SearchParameter, this method will resolve the components
150         * in the order they appear in the composite parameter definition. The return objects
151         * are a record containing both the component from the composite parameter definition,
152         * but also the corresponding SearchParameter definition for the target of the component.
153         *
154         * @param theSearchParamRegistry The active SearchParameter registry
155         * @param theCompositeParameter The composite search parameter
156         */
157        @Nonnull
158        public static List<ComponentAndCorrespondingParam> resolveCompositeComponents(
159                        ISearchParamRegistry theSearchParamRegistry, RuntimeSearchParam theCompositeParameter) {
160                List<ComponentAndCorrespondingParam> compositeList = new ArrayList<>();
161                List<RuntimeSearchParam.Component> components = theCompositeParameter.getComponents();
162                for (RuntimeSearchParam.Component next : components) {
163                        String url = next.getReference();
164                        RuntimeSearchParam componentParam = theSearchParamRegistry.getActiveSearchParamByUrl(
165                                        url, ISearchParamRegistry.SearchParamLookupContextEnum.ALL);
166                        if (componentParam == null) {
167                                throw new InternalErrorException(Msg.code(499) + "Can not find SearchParameter: " + url);
168                        }
169                        compositeList.add(new ComponentAndCorrespondingParam(next, componentParam));
170                }
171                return compositeList;
172        }
173
174        private static Class<?> getCompositeBindingClass(
175                        RestSearchParameterTypeEnum paramType, String theUnqualifiedParamName) {
176
177                switch (paramType) {
178                        case DATE:
179                                return DateParam.class;
180                        case NUMBER:
181                                return NumberParam.class;
182                        case QUANTITY:
183                                return QuantityParam.class;
184                        case REFERENCE:
185                                return ReferenceParam.class;
186                        case STRING:
187                                return StringParam.class;
188                        case TOKEN:
189                                return TokenParam.class;
190                        case URI:
191                                return UriParam.class;
192                        case HAS:
193                                return HasParam.class;
194                        case SPECIAL:
195                                return SpecialParam.class;
196
197                        case COMPOSITE:
198                        default:
199                                throw new IllegalArgumentException(Msg.code(500) + "Parameter '" + theUnqualifiedParamName
200                                                + "' has type " + paramType + " which is currently not supported.");
201                }
202        }
203
204        /**
205         * Given a component for a composite or combo SearchParameter (as returned by {@link #resolveCompositeComponents(ISearchParamRegistry, RuntimeSearchParam)})
206         * determines the type associated with the target parameter.
207         */
208        public static RestSearchParameterTypeEnum getParameterTypeForComposite(
209                        ISearchParamRegistry theSearchParamRegistry, ComponentAndCorrespondingParam theComponentAndParam) {
210                String chain = theComponentAndParam.getComponent().getComboUpliftChain();
211                if (chain != null) {
212                        RuntimeSearchParam targetParameter = theComponentAndParam.getComponentParameter();
213                        for (String target : targetParameter.getTargets()) {
214                                RuntimeSearchParam chainTargetParam = theSearchParamRegistry.getActiveSearchParam(
215                                                target, chain, ISearchParamRegistry.SearchParamLookupContextEnum.ALL);
216                                if (chainTargetParam != null) {
217                                        return chainTargetParam.getParamType();
218                                }
219                        }
220                        // Fallback if we can't find a chain target
221                        return RestSearchParameterTypeEnum.TOKEN;
222                } else {
223                        return theComponentAndParam.getComponentParameter().getParamType();
224                }
225        }
226
227        /**
228         * Return type for {@link #resolveCompositeComponents(ISearchParamRegistry, RuntimeSearchParam)}
229         *
230         * @since 8.6.0
231         */
232        public static class ComponentAndCorrespondingParam {
233
234                private final String myCombinedParamName;
235                private final RuntimeSearchParam.Component myComponent;
236                private final RuntimeSearchParam myComponentParameter;
237                private final String myParamName;
238                private final String myChain;
239
240                /**
241                 * Constructor
242                 */
243                ComponentAndCorrespondingParam(
244                                @Nonnull RuntimeSearchParam.Component theComponent, @Nonnull RuntimeSearchParam theComponentParameter) {
245                        this.myComponent = theComponent;
246                        this.myComponentParameter = theComponentParameter;
247
248                        int dotIdx = theComponentParameter.getName().indexOf(".");
249                        if (dotIdx != -1) {
250                                myParamName = theComponentParameter.getName().substring(0, dotIdx);
251                                myChain = theComponentParameter.getName().substring(dotIdx + 1);
252                                this.myCombinedParamName = theComponentParameter.getName();
253                        } else {
254                                myParamName = theComponentParameter.getName();
255                                myChain = theComponent.getComboUpliftChain();
256                                if (myChain != null) {
257                                        myCombinedParamName = myParamName + "." + myChain;
258                                } else {
259                                        myCombinedParamName = myParamName;
260                                }
261                        }
262                }
263
264                /**
265                 * The component definition in the source composite SearchParameter
266                 */
267                @Nonnull
268                public RuntimeSearchParam.Component getComponent() {
269                        return myComponent;
270                }
271
272                /**
273                 * The target definition referred to by the {@link #getComponent() component} in the source composite SearchParameter
274                 */
275                @Nonnull
276                public RuntimeSearchParam getComponentParameter() {
277                        return myComponentParameter;
278                }
279
280                /**
281                 * The parameter name, without any chained parameter names
282                 */
283                @Nonnull
284                public String getParamName() {
285                        return myParamName;
286                }
287
288                /**
289                 * The chain portion of the component parameter if any, or <code>null</code> if not. Excludes
290                 * the leading period, so if the component parameter is "subject.name", this will return "name".
291                 */
292                @Nullable
293                public String getChain() {
294                        return myChain;
295                }
296
297                /**
298                 * The combined parameter name, including any chained parameter names, e.g. "subject.name"
299                 */
300                @Nonnull
301                public String getCombinedParamName() {
302                        return myCombinedParamName;
303                }
304        }
305}