001package ca.uhn.fhir.rest.server.method; 002 003/* 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022import ca.uhn.fhir.i18n.Msg; 023import java.util.*; 024 025import org.apache.commons.lang3.builder.ToStringBuilder; 026import org.hl7.fhir.instance.model.api.IBaseResource; 027import org.hl7.fhir.instance.model.api.IPrimitiveType; 028 029import ca.uhn.fhir.context.*; 030import ca.uhn.fhir.model.api.*; 031import ca.uhn.fhir.model.base.composite.BaseIdentifierDt; 032import ca.uhn.fhir.model.base.composite.BaseQuantityDt; 033import ca.uhn.fhir.model.primitive.StringDt; 034import ca.uhn.fhir.rest.annotation.OptionalParam; 035import ca.uhn.fhir.rest.api.*; 036import ca.uhn.fhir.rest.param.*; 037import ca.uhn.fhir.rest.param.binder.*; 038import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 039import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 040import ca.uhn.fhir.util.CollectionUtil; 041import ca.uhn.fhir.util.ReflectionUtil; 042 043public class SearchParameter extends BaseQueryParameter { 044 045 private static final String EMPTY_STRING = ""; 046 private static final HashMap<RestSearchParameterTypeEnum, Set<String>> ourParamQualifiers; 047 private static final HashMap<Class<?>, RestSearchParameterTypeEnum> ourParamTypes; 048 static final String QUALIFIER_ANY_TYPE = ":*"; 049 050 static { 051 ourParamTypes = new HashMap<>(); 052 ourParamQualifiers = new HashMap<>(); 053 054 ourParamTypes.put(StringParam.class, RestSearchParameterTypeEnum.STRING); 055 ourParamTypes.put(StringOrListParam.class, RestSearchParameterTypeEnum.STRING); 056 ourParamTypes.put(StringAndListParam.class, RestSearchParameterTypeEnum.STRING); 057 ourParamQualifiers.put(RestSearchParameterTypeEnum.STRING, CollectionUtil.newSet(Constants.PARAMQUALIFIER_STRING_EXACT, Constants.PARAMQUALIFIER_STRING_CONTAINS, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); 058 059 ourParamTypes.put(UriParam.class, RestSearchParameterTypeEnum.URI); 060 ourParamTypes.put(UriOrListParam.class, RestSearchParameterTypeEnum.URI); 061 ourParamTypes.put(UriAndListParam.class, RestSearchParameterTypeEnum.URI); 062 // TODO: are these right for URI? 063 ourParamQualifiers.put(RestSearchParameterTypeEnum.URI, CollectionUtil.newSet(Constants.PARAMQUALIFIER_STRING_EXACT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); 064 065 ourParamTypes.put(TokenParam.class, RestSearchParameterTypeEnum.TOKEN); 066 ourParamTypes.put(TokenOrListParam.class, RestSearchParameterTypeEnum.TOKEN); 067 ourParamTypes.put(TokenAndListParam.class, RestSearchParameterTypeEnum.TOKEN); 068 ourParamQualifiers.put(RestSearchParameterTypeEnum.TOKEN, CollectionUtil.newSet(Constants.PARAMQUALIFIER_TOKEN_TEXT, Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); 069 070 ourParamTypes.put(DateParam.class, RestSearchParameterTypeEnum.DATE); 071 ourParamTypes.put(DateOrListParam.class, RestSearchParameterTypeEnum.DATE); 072 ourParamTypes.put(DateAndListParam.class, RestSearchParameterTypeEnum.DATE); 073 ourParamTypes.put(DateRangeParam.class, RestSearchParameterTypeEnum.DATE); 074 ourParamQualifiers.put(RestSearchParameterTypeEnum.DATE, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); 075 076 ourParamTypes.put(QuantityParam.class, RestSearchParameterTypeEnum.QUANTITY); 077 ourParamTypes.put(QuantityOrListParam.class, RestSearchParameterTypeEnum.QUANTITY); 078 ourParamTypes.put(QuantityAndListParam.class, RestSearchParameterTypeEnum.QUANTITY); 079 ourParamQualifiers.put(RestSearchParameterTypeEnum.QUANTITY, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); 080 081 ourParamTypes.put(NumberParam.class, RestSearchParameterTypeEnum.NUMBER); 082 ourParamTypes.put(NumberOrListParam.class, RestSearchParameterTypeEnum.NUMBER); 083 ourParamTypes.put(NumberAndListParam.class, RestSearchParameterTypeEnum.NUMBER); 084 ourParamQualifiers.put(RestSearchParameterTypeEnum.NUMBER, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); 085 086 ourParamTypes.put(ReferenceParam.class, RestSearchParameterTypeEnum.REFERENCE); 087 ourParamTypes.put(ReferenceOrListParam.class, RestSearchParameterTypeEnum.REFERENCE); 088 ourParamTypes.put(ReferenceAndListParam.class, RestSearchParameterTypeEnum.REFERENCE); 089 // --vvvv-- no empty because that gets added from OptionalParam#chainWhitelist 090 ourParamQualifiers.put(RestSearchParameterTypeEnum.REFERENCE, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING)); 091 092 ourParamTypes.put(CompositeParam.class, RestSearchParameterTypeEnum.COMPOSITE); 093 ourParamTypes.put(CompositeOrListParam.class, RestSearchParameterTypeEnum.COMPOSITE); 094 ourParamTypes.put(CompositeAndListParam.class, RestSearchParameterTypeEnum.COMPOSITE); 095 ourParamQualifiers.put(RestSearchParameterTypeEnum.COMPOSITE, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING, EMPTY_STRING)); 096 097 ourParamTypes.put(HasParam.class, RestSearchParameterTypeEnum.HAS); 098 ourParamTypes.put(HasOrListParam.class, RestSearchParameterTypeEnum.HAS); 099 ourParamTypes.put(HasAndListParam.class, RestSearchParameterTypeEnum.HAS); 100 101 ourParamTypes.put(SpecialParam.class, RestSearchParameterTypeEnum.SPECIAL); 102 ourParamTypes.put(SpecialOrListParam.class, RestSearchParameterTypeEnum.SPECIAL); 103 ourParamTypes.put(SpecialAndListParam.class, RestSearchParameterTypeEnum.SPECIAL); 104 ourParamQualifiers.put(RestSearchParameterTypeEnum.SPECIAL, CollectionUtil.newSet(Constants.PARAMQUALIFIER_MISSING)); 105 106 } 107 108 private List<Class<? extends IQueryParameterType>> myCompositeTypes = Collections.emptyList(); 109 private List<Class<? extends IBaseResource>> myDeclaredTypes; 110 private String myDescription; 111 private String myName; 112 private IParamBinder<?> myParamBinder; 113 private RestSearchParameterTypeEnum myParamType; 114 private Set<String> myQualifierBlacklist; 115 private Set<String> myQualifierWhitelist; 116 private boolean myRequired; 117 private Class<?> myType; 118 private boolean mySupportsRepetition = false; 119 120 public SearchParameter() { 121 } 122 123 public SearchParameter(String theName, boolean theRequired) { 124 this.myName = theName; 125 this.myRequired = theRequired; 126 } 127 128 /* 129 * (non-Javadoc) 130 * 131 * @see ca.uhn.fhir.rest.param.IParameter#encode(java.lang.Object) 132 */ 133 @Override 134 public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException { 135 ArrayList<QualifiedParamList> retVal = new ArrayList<>(); 136 137 // TODO: declaring method should probably have a generic type.. 138 @SuppressWarnings("rawtypes") 139 IParamBinder paramBinder = myParamBinder; 140 141 @SuppressWarnings("unchecked") 142 List<IQueryParameterOr<?>> val = paramBinder.encode(theContext, theObject); 143 for (IQueryParameterOr<?> nextOr : val) { 144 retVal.add(new QualifiedParamList(nextOr, theContext)); 145 } 146 147 return retVal; 148 } 149 150 public List<Class<? extends IBaseResource>> getDeclaredTypes() { 151 return Collections.unmodifiableList(myDeclaredTypes); 152 } 153 154 public String getDescription() { 155 return myDescription; 156 } 157 158 /* 159 * (non-Javadoc) 160 * 161 * @see ca.uhn.fhir.rest.param.IParameter#getName() 162 */ 163 @Override 164 public String getName() { 165 return myName; 166 } 167 168 @Override 169 public RestSearchParameterTypeEnum getParamType() { 170 return myParamType; 171 } 172 173 @Override 174 public Set<String> getQualifierBlacklist() { 175 return myQualifierBlacklist; 176 } 177 178 @Override 179 public Set<String> getQualifierWhitelist() { 180 return myQualifierWhitelist; 181 } 182 183 public Class<?> getType() { 184 return myType; 185 } 186 187 @Override 188 public boolean handlesMissing() { 189 return false; 190 } 191 192 @Override 193 public boolean isRequired() { 194 return myRequired; 195 } 196 197 /* 198 * (non-Javadoc) 199 * 200 * @see ca.uhn.fhir.rest.param.IParameter#parse(java.util.List) 201 */ 202 @Override 203 public Object parse(FhirContext theContext, List<QualifiedParamList> theString) throws InternalErrorException, InvalidRequestException { 204 return myParamBinder.parse(theContext, getName(), theString); 205 } 206 207 public void setChainLists(String[] theChainWhitelist, String[] theChainBlacklist) { 208 myQualifierWhitelist = new HashSet<>(theChainWhitelist.length); 209 myQualifierWhitelist.add(QUALIFIER_ANY_TYPE); 210 211 for (String nextChain : theChainWhitelist) { 212 if (nextChain.equals(OptionalParam.ALLOW_CHAIN_ANY)) { 213 myQualifierWhitelist.add('.' + OptionalParam.ALLOW_CHAIN_ANY); 214 } else if (nextChain.equals(EMPTY_STRING)) { 215 myQualifierWhitelist.add("."); 216 } else { 217 myQualifierWhitelist.add('.' + nextChain); 218 } 219 } 220 221 if (theChainBlacklist.length > 0) { 222 myQualifierBlacklist = new HashSet<>(theChainBlacklist.length); 223 for (String next : theChainBlacklist) { 224 if (next.equals(EMPTY_STRING)) { 225 myQualifierBlacklist.add(EMPTY_STRING); 226 } else { 227 myQualifierBlacklist.add('.' + next); 228 } 229 } 230 } 231 } 232 233 public void setCompositeTypes(Class<? extends IQueryParameterType>[] theCompositeTypes) { 234 myCompositeTypes = Arrays.asList(theCompositeTypes); 235 } 236 237 public void setDeclaredTypes(Class<? extends IBaseResource>[] theTypes) { 238 myDeclaredTypes = Arrays.asList(theTypes); 239 } 240 241 public void setDescription(String theDescription) { 242 myDescription = theDescription; 243 } 244 245 public void setName(String name) { 246 this.myName = name; 247 } 248 249 public void setRequired(boolean required) { 250 this.myRequired = required; 251 } 252 253 @Override 254 protected boolean supportsRepetition() { 255 return mySupportsRepetition; 256 } 257 258 @SuppressWarnings("unchecked") 259 public void setType(FhirContext theContext, final Class<?> theType, Class<? extends Collection<?>> theInnerCollectionType, Class<? extends Collection<?>> theOuterCollectionType) { 260 261 262 this.myType = theType; 263 if (IQueryParameterType.class.isAssignableFrom(theType)) { 264 myParamBinder = new QueryParameterTypeBinder((Class<? extends IQueryParameterType>) theType, myCompositeTypes); 265 } else if (IQueryParameterOr.class.isAssignableFrom(theType)) { 266 myParamBinder = new QueryParameterOrBinder((Class<? extends IQueryParameterOr<?>>) theType, myCompositeTypes); 267 } else if (IQueryParameterAnd.class.isAssignableFrom(theType)) { 268 myParamBinder = new QueryParameterAndBinder((Class<? extends IQueryParameterAnd<?>>) theType, myCompositeTypes); 269 mySupportsRepetition = true; 270 } else if (String.class.equals(theType)) { 271 myParamBinder = new StringBinder(); 272 myParamType = RestSearchParameterTypeEnum.STRING; 273 } else if (Date.class.equals(theType)) { 274 myParamBinder = new DateBinder(); 275 myParamType = RestSearchParameterTypeEnum.DATE; 276 } else if (Calendar.class.equals(theType)) { 277 myParamBinder = new CalendarBinder(); 278 myParamType = RestSearchParameterTypeEnum.DATE; 279 } else if (IPrimitiveType.class.isAssignableFrom(theType) && ReflectionUtil.isInstantiable(theType)) { 280 RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) theContext.getElementDefinition((Class<? extends IPrimitiveType<?>>) theType); 281 if (def.getNativeType() != null) { 282 if (def.getNativeType().equals(Date.class)) { 283 myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType); 284 myParamType = RestSearchParameterTypeEnum.DATE; 285 } else if (def.getNativeType().equals(String.class)) { 286 myParamBinder = new FhirPrimitiveBinder((Class<IPrimitiveType<?>>) theType); 287 myParamType = RestSearchParameterTypeEnum.STRING; 288 } 289 } 290 } else { 291 throw new ConfigurationException(Msg.code(354) + "Unsupported data type for parameter: " + theType.getCanonicalName()); 292 } 293 294 RestSearchParameterTypeEnum typeEnum = ourParamTypes.get(theType); 295 if (typeEnum != null) { 296 Set<String> builtInQualifiers = ourParamQualifiers.get(typeEnum); 297 if (builtInQualifiers != null) { 298 if (myQualifierWhitelist != null) { 299 HashSet<String> qualifierWhitelist = new HashSet<>(); 300 qualifierWhitelist.addAll(myQualifierWhitelist); 301 qualifierWhitelist.addAll(builtInQualifiers); 302 myQualifierWhitelist = qualifierWhitelist; 303 } else { 304 myQualifierWhitelist = Collections.unmodifiableSet(builtInQualifiers); 305 } 306 } 307 } 308 309 if (myParamType == null) { 310 myParamType = typeEnum; 311 } 312 313 if (myParamType != null) { 314 // ok 315 } else if (StringDt.class.isAssignableFrom(theType)) { 316 myParamType = RestSearchParameterTypeEnum.STRING; 317 } else if (BaseIdentifierDt.class.isAssignableFrom(theType)) { 318 myParamType = RestSearchParameterTypeEnum.TOKEN; 319 } else if (BaseQuantityDt.class.isAssignableFrom(theType)) { 320 myParamType = RestSearchParameterTypeEnum.QUANTITY; 321 } else if (ReferenceParam.class.isAssignableFrom(theType)) { 322 myParamType = RestSearchParameterTypeEnum.REFERENCE; 323 } else if (HasParam.class.isAssignableFrom(theType)) { 324 myParamType = RestSearchParameterTypeEnum.STRING; 325 } else { 326 throw new ConfigurationException(Msg.code(355) + "Unknown search parameter type: " + theType); 327 } 328 329 // NB: Once this is enabled, we should return true from handlesMissing if 330 // it's a collection theType 331 // if (theInnerCollectionType != null) { 332 // this.parser = new CollectionBinder(this.parser, theInnerCollectionType); 333 // } 334 // 335 // if (theOuterCollectionType != null) { 336 // this.parser = new CollectionBinder(this.parser, theOuterCollectionType); 337 // } 338 339 } 340 341 @Override 342 public String toString() { 343 ToStringBuilder retVal = new ToStringBuilder(this); 344 retVal.append("name", myName); 345 retVal.append("required", myRequired); 346 return retVal.toString(); 347 } 348 349}