
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}