001/* 002 * #%L 003 * HAPI FHIR - Server Framework 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.rest.server.method; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.model.api.IQueryParameterAnd; 027import ca.uhn.fhir.model.api.IQueryParameterOr; 028import ca.uhn.fhir.model.api.IQueryParameterType; 029import ca.uhn.fhir.model.base.composite.BaseIdentifierDt; 030import ca.uhn.fhir.model.base.composite.BaseQuantityDt; 031import ca.uhn.fhir.model.primitive.StringDt; 032import ca.uhn.fhir.rest.annotation.OptionalParam; 033import ca.uhn.fhir.rest.api.Constants; 034import ca.uhn.fhir.rest.api.QualifiedParamList; 035import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 036import ca.uhn.fhir.rest.param.CompositeAndListParam; 037import ca.uhn.fhir.rest.param.CompositeOrListParam; 038import ca.uhn.fhir.rest.param.CompositeParam; 039import ca.uhn.fhir.rest.param.DateAndListParam; 040import ca.uhn.fhir.rest.param.DateOrListParam; 041import ca.uhn.fhir.rest.param.DateParam; 042import ca.uhn.fhir.rest.param.DateRangeParam; 043import ca.uhn.fhir.rest.param.HasAndListParam; 044import ca.uhn.fhir.rest.param.HasOrListParam; 045import ca.uhn.fhir.rest.param.HasParam; 046import ca.uhn.fhir.rest.param.NumberAndListParam; 047import ca.uhn.fhir.rest.param.NumberOrListParam; 048import ca.uhn.fhir.rest.param.NumberParam; 049import ca.uhn.fhir.rest.param.QuantityAndListParam; 050import ca.uhn.fhir.rest.param.QuantityOrListParam; 051import ca.uhn.fhir.rest.param.QuantityParam; 052import ca.uhn.fhir.rest.param.ReferenceAndListParam; 053import ca.uhn.fhir.rest.param.ReferenceOrListParam; 054import ca.uhn.fhir.rest.param.ReferenceParam; 055import ca.uhn.fhir.rest.param.SpecialAndListParam; 056import ca.uhn.fhir.rest.param.SpecialOrListParam; 057import ca.uhn.fhir.rest.param.SpecialParam; 058import ca.uhn.fhir.rest.param.StringAndListParam; 059import ca.uhn.fhir.rest.param.StringOrListParam; 060import ca.uhn.fhir.rest.param.StringParam; 061import ca.uhn.fhir.rest.param.TokenAndListParam; 062import ca.uhn.fhir.rest.param.TokenOrListParam; 063import ca.uhn.fhir.rest.param.TokenParam; 064import ca.uhn.fhir.rest.param.UriAndListParam; 065import ca.uhn.fhir.rest.param.UriOrListParam; 066import ca.uhn.fhir.rest.param.UriParam; 067import ca.uhn.fhir.rest.param.binder.CalendarBinder; 068import ca.uhn.fhir.rest.param.binder.DateBinder; 069import ca.uhn.fhir.rest.param.binder.FhirPrimitiveBinder; 070import ca.uhn.fhir.rest.param.binder.IParamBinder; 071import ca.uhn.fhir.rest.param.binder.QueryParameterAndBinder; 072import ca.uhn.fhir.rest.param.binder.QueryParameterOrBinder; 073import ca.uhn.fhir.rest.param.binder.QueryParameterTypeBinder; 074import ca.uhn.fhir.rest.param.binder.StringBinder; 075import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 076import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 077import ca.uhn.fhir.util.ReflectionUtil; 078import org.apache.commons.lang3.builder.ToStringBuilder; 079import org.hl7.fhir.instance.model.api.IBaseResource; 080import org.hl7.fhir.instance.model.api.IPrimitiveType; 081 082import java.util.ArrayList; 083import java.util.Arrays; 084import java.util.Calendar; 085import java.util.Collection; 086import java.util.Collections; 087import java.util.Date; 088import java.util.HashMap; 089import java.util.HashSet; 090import java.util.List; 091import java.util.Set; 092 093public class SearchParameter extends BaseQueryParameter { 094 095 private static final String EMPTY_STRING = ""; 096 private static final HashMap<RestSearchParameterTypeEnum, Set<String>> ourParamQualifiers; 097 private static final HashMap<Class<?>, RestSearchParameterTypeEnum> ourParamTypes; 098 static final String QUALIFIER_ANY_TYPE = ":*"; 099 100 static { 101 ourParamTypes = new HashMap<>(); 102 ourParamQualifiers = new HashMap<>(); 103 104 ourParamTypes.put(StringParam.class, RestSearchParameterTypeEnum.STRING); 105 ourParamTypes.put(StringOrListParam.class, RestSearchParameterTypeEnum.STRING); 106 ourParamTypes.put(StringAndListParam.class, RestSearchParameterTypeEnum.STRING); 107 ourParamQualifiers.put(RestSearchParameterTypeEnum.STRING, Set.of(new String[] { 108 Constants.PARAMQUALIFIER_STRING_EXACT, 109 Constants.PARAMQUALIFIER_STRING_CONTAINS, 110 Constants.PARAMQUALIFIER_MISSING, 111 EMPTY_STRING 112 })); 113 114 ourParamTypes.put(UriParam.class, RestSearchParameterTypeEnum.URI); 115 ourParamTypes.put(UriOrListParam.class, RestSearchParameterTypeEnum.URI); 116 ourParamTypes.put(UriAndListParam.class, RestSearchParameterTypeEnum.URI); 117 // TODO: are these right for URI? 118 ourParamQualifiers.put(RestSearchParameterTypeEnum.URI, Set.of(new String[] { 119 Constants.PARAMQUALIFIER_STRING_EXACT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING 120 })); 121 122 ourParamTypes.put(TokenParam.class, RestSearchParameterTypeEnum.TOKEN); 123 ourParamTypes.put(TokenOrListParam.class, RestSearchParameterTypeEnum.TOKEN); 124 ourParamTypes.put(TokenAndListParam.class, RestSearchParameterTypeEnum.TOKEN); 125 ourParamQualifiers.put(RestSearchParameterTypeEnum.TOKEN, Set.of(new String[] { 126 Constants.PARAMQUALIFIER_TOKEN_TEXT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING 127 })); 128 129 ourParamTypes.put(DateParam.class, RestSearchParameterTypeEnum.DATE); 130 ourParamTypes.put(DateOrListParam.class, RestSearchParameterTypeEnum.DATE); 131 ourParamTypes.put(DateAndListParam.class, RestSearchParameterTypeEnum.DATE); 132 ourParamTypes.put(DateRangeParam.class, RestSearchParameterTypeEnum.DATE); 133 ourParamQualifiers.put( 134 RestSearchParameterTypeEnum.DATE, 135 Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING})); 136 137 ourParamTypes.put(QuantityParam.class, RestSearchParameterTypeEnum.QUANTITY); 138 ourParamTypes.put(QuantityOrListParam.class, RestSearchParameterTypeEnum.QUANTITY); 139 ourParamTypes.put(QuantityAndListParam.class, RestSearchParameterTypeEnum.QUANTITY); 140 ourParamQualifiers.put( 141 RestSearchParameterTypeEnum.QUANTITY, 142 Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING})); 143 144 ourParamTypes.put(NumberParam.class, RestSearchParameterTypeEnum.NUMBER); 145 ourParamTypes.put(NumberOrListParam.class, RestSearchParameterTypeEnum.NUMBER); 146 ourParamTypes.put(NumberAndListParam.class, RestSearchParameterTypeEnum.NUMBER); 147 ourParamQualifiers.put( 148 RestSearchParameterTypeEnum.NUMBER, 149 Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING})); 150 151 ourParamTypes.put(ReferenceParam.class, RestSearchParameterTypeEnum.REFERENCE); 152 ourParamTypes.put(ReferenceOrListParam.class, RestSearchParameterTypeEnum.REFERENCE); 153 ourParamTypes.put(ReferenceAndListParam.class, RestSearchParameterTypeEnum.REFERENCE); 154 // --vvvv-- no empty because that gets added from OptionalParam#chainWhitelist 155 ourParamQualifiers.put( 156 RestSearchParameterTypeEnum.REFERENCE, Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING})); 157 158 ourParamTypes.put(CompositeParam.class, RestSearchParameterTypeEnum.COMPOSITE); 159 ourParamTypes.put(CompositeOrListParam.class, RestSearchParameterTypeEnum.COMPOSITE); 160 ourParamTypes.put(CompositeAndListParam.class, RestSearchParameterTypeEnum.COMPOSITE); 161 ourParamQualifiers.put( 162 RestSearchParameterTypeEnum.COMPOSITE, 163 Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING})); 164 165 ourParamTypes.put(HasParam.class, RestSearchParameterTypeEnum.HAS); 166 ourParamTypes.put(HasOrListParam.class, RestSearchParameterTypeEnum.HAS); 167 ourParamTypes.put(HasAndListParam.class, RestSearchParameterTypeEnum.HAS); 168 169 ourParamTypes.put(SpecialParam.class, RestSearchParameterTypeEnum.SPECIAL); 170 ourParamTypes.put(SpecialOrListParam.class, RestSearchParameterTypeEnum.SPECIAL); 171 ourParamTypes.put(SpecialAndListParam.class, RestSearchParameterTypeEnum.SPECIAL); 172 ourParamQualifiers.put( 173 RestSearchParameterTypeEnum.SPECIAL, Set.of(new String[] {Constants.PARAMQUALIFIER_MISSING})); 174 } 175 176 private List<Class<? extends IQueryParameterType>> myCompositeTypes = Collections.emptyList(); 177 private List<Class<? extends IBaseResource>> myDeclaredTypes; 178 private String myDescription; 179 private String myName; 180 private IParamBinder<?> myParamBinder; 181 private RestSearchParameterTypeEnum myParamType; 182 private Set<String> myQualifierBlacklist; 183 private Set<String> myQualifierWhitelist; 184 private boolean myRequired; 185 private Class<?> myType; 186 private boolean mySupportsRepetition = false; 187 188 public SearchParameter() {} 189 190 public SearchParameter(String theName, boolean theRequired) { 191 this.myName = theName; 192 this.myRequired = theRequired; 193 } 194 195 /* 196 * (non-Javadoc) 197 * 198 * @see ca.uhn.fhir.rest.param.IParameter#encode(java.lang.Object) 199 */ 200 @Override 201 public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException { 202 ArrayList<QualifiedParamList> retVal = new ArrayList<>(); 203 204 // TODO: declaring method should probably have a generic type.. 205 @SuppressWarnings("rawtypes") 206 IParamBinder paramBinder = myParamBinder; 207 208 @SuppressWarnings("unchecked") 209 List<IQueryParameterOr<?>> val = paramBinder.encode(theContext, theObject); 210 for (IQueryParameterOr<?> nextOr : val) { 211 retVal.add(new QualifiedParamList(nextOr, theContext)); 212 } 213 214 return retVal; 215 } 216 217 public List<Class<? extends IBaseResource>> getDeclaredTypes() { 218 return Collections.unmodifiableList(myDeclaredTypes); 219 } 220 221 public String getDescription() { 222 return myDescription; 223 } 224 225 /* 226 * (non-Javadoc) 227 * 228 * @see ca.uhn.fhir.rest.param.IParameter#getName() 229 */ 230 @Override 231 public String getName() { 232 return myName; 233 } 234 235 @Override 236 public RestSearchParameterTypeEnum getParamType() { 237 return myParamType; 238 } 239 240 @Override 241 public Set<String> getQualifierBlacklist() { 242 return myQualifierBlacklist; 243 } 244 245 @Override 246 public Set<String> getQualifierWhitelist() { 247 return myQualifierWhitelist; 248 } 249 250 public Class<?> getType() { 251 return myType; 252 } 253 254 @Override 255 public boolean handlesMissing() { 256 return false; 257 } 258 259 @Override 260 public boolean isRequired() { 261 return myRequired; 262 } 263 264 /* 265 * (non-Javadoc) 266 * 267 * @see ca.uhn.fhir.rest.param.IParameter#parse(java.util.List) 268 */ 269 @Override 270 public Object parse(FhirContext theContext, List<QualifiedParamList> theString) 271 throws InternalErrorException, InvalidRequestException { 272 return myParamBinder.parse(theContext, getName(), theString); 273 } 274 275 public void setChainLists(String[] theChainWhitelist, String[] theChainBlacklist) { 276 myQualifierWhitelist = new HashSet<>(theChainWhitelist.length); 277 myQualifierWhitelist.add(QUALIFIER_ANY_TYPE); 278 279 for (String nextChain : theChainWhitelist) { 280 if (nextChain.equals(OptionalParam.ALLOW_CHAIN_ANY)) { 281 myQualifierWhitelist.add('.' + OptionalParam.ALLOW_CHAIN_ANY); 282 } else if (nextChain.equals(EMPTY_STRING)) { 283 myQualifierWhitelist.add("."); 284 } else { 285 myQualifierWhitelist.add('.' + nextChain); 286 } 287 } 288 289 if (theChainBlacklist.length > 0) { 290 myQualifierBlacklist = new HashSet<>(theChainBlacklist.length); 291 for (String next : theChainBlacklist) { 292 if (next.equals(EMPTY_STRING)) { 293 myQualifierBlacklist.add(EMPTY_STRING); 294 } else { 295 myQualifierBlacklist.add('.' + next); 296 } 297 } 298 } 299 } 300 301 public void setCompositeTypes(Class<? extends IQueryParameterType>[] theCompositeTypes) { 302 myCompositeTypes = Arrays.asList(theCompositeTypes); 303 } 304 305 public void setDeclaredTypes(Class<? extends IBaseResource>[] theTypes) { 306 myDeclaredTypes = Arrays.asList(theTypes); 307 } 308 309 public void setDescription(String theDescription) { 310 myDescription = theDescription; 311 } 312 313 public void setName(String name) { 314 this.myName = name; 315 } 316 317 public void setRequired(boolean required) { 318 this.myRequired = required; 319 } 320 321 @Override 322 protected boolean supportsRepetition() { 323 return mySupportsRepetition; 324 } 325 326 @SuppressWarnings("unchecked") 327 public void setType( 328 FhirContext theContext, 329 final Class<?> theType, 330 Class<? extends Collection<?>> theInnerCollectionType, 331 Class<? extends Collection<?>> theOuterCollectionType) { 332 333 this.myType = theType; 334 if (IQueryParameterType.class.isAssignableFrom(theType)) { 335 myParamBinder = 336 new QueryParameterTypeBinder((Class<? extends IQueryParameterType>) theType, myCompositeTypes); 337 } else if (IQueryParameterOr.class.isAssignableFrom(theType)) { 338 myParamBinder = 339 new QueryParameterOrBinder((Class<? extends IQueryParameterOr<?>>) theType, myCompositeTypes); 340 } else if (IQueryParameterAnd.class.isAssignableFrom(theType)) { 341 myParamBinder = 342 new QueryParameterAndBinder((Class<? extends IQueryParameterAnd<?>>) theType, myCompositeTypes); 343 mySupportsRepetition = true; 344 } else if (String.class.equals(theType)) { 345 myParamBinder = new StringBinder(); 346 myParamType = RestSearchParameterTypeEnum.STRING; 347 } else if (Date.class.equals(theType)) { 348 myParamBinder = new DateBinder(); 349 myParamType = RestSearchParameterTypeEnum.DATE; 350 } else if (Calendar.class.equals(theType)) { 351 myParamBinder = new CalendarBinder(); 352 myParamType = RestSearchParameterTypeEnum.DATE; 353 } else if (IPrimitiveType.class.isAssignableFrom(theType) && ReflectionUtil.isInstantiable(theType)) { 354 RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) 355 theContext.getElementDefinition((Class<? extends IPrimitiveType<?>>) theType); 356 if (def.getNativeType() != null) { 357 if (def.getNativeType().equals(Date.class)) { 358 myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType); 359 myParamType = RestSearchParameterTypeEnum.DATE; 360 } else if (def.getNativeType().equals(String.class)) { 361 myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType); 362 myParamType = RestSearchParameterTypeEnum.STRING; 363 } 364 } 365 } else { 366 throw new ConfigurationException( 367 Msg.code(354) + "Unsupported data type for parameter: " + theType.getCanonicalName()); 368 } 369 370 RestSearchParameterTypeEnum typeEnum = ourParamTypes.get(theType); 371 if (typeEnum != null) { 372 Set<String> builtInQualifiers = ourParamQualifiers.get(typeEnum); 373 if (builtInQualifiers != null) { 374 if (myQualifierWhitelist != null) { 375 HashSet<String> qualifierWhitelist = new HashSet<>(); 376 qualifierWhitelist.addAll(myQualifierWhitelist); 377 qualifierWhitelist.addAll(builtInQualifiers); 378 myQualifierWhitelist = qualifierWhitelist; 379 } else { 380 myQualifierWhitelist = Collections.unmodifiableSet(builtInQualifiers); 381 } 382 } 383 } 384 385 if (myParamType == null) { 386 myParamType = typeEnum; 387 } 388 389 if (myParamType != null) { 390 // ok 391 } else if (StringDt.class.isAssignableFrom(theType)) { 392 myParamType = RestSearchParameterTypeEnum.STRING; 393 } else if (BaseIdentifierDt.class.isAssignableFrom(theType)) { 394 myParamType = RestSearchParameterTypeEnum.TOKEN; 395 } else if (BaseQuantityDt.class.isAssignableFrom(theType)) { 396 myParamType = RestSearchParameterTypeEnum.QUANTITY; 397 } else if (ReferenceParam.class.isAssignableFrom(theType)) { 398 myParamType = RestSearchParameterTypeEnum.REFERENCE; 399 } else if (HasParam.class.isAssignableFrom(theType)) { 400 myParamType = RestSearchParameterTypeEnum.STRING; 401 } else { 402 throw new ConfigurationException(Msg.code(355) + "Unknown search parameter type: " + theType); 403 } 404 405 // NB: Once this is enabled, we should return true from handlesMissing if 406 // it's a collection theType 407 // if (theInnerCollectionType != null) { 408 // this.parser = new CollectionBinder(this.parser, theInnerCollectionType); 409 // } 410 // 411 // if (theOuterCollectionType != null) { 412 // this.parser = new CollectionBinder(this.parser, theOuterCollectionType); 413 // } 414 415 } 416 417 @Override 418 public String toString() { 419 ToStringBuilder retVal = new ToStringBuilder(this); 420 retVal.append("name", myName); 421 retVal.append("required", myRequired); 422 return retVal.toString(); 423 } 424}