001package ca.uhn.fhir.rest.server.method; 002 003/* 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2019 University Health Network 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 */ 022 023import ca.uhn.fhir.context.ConfigurationException; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.model.api.annotation.Description; 026import ca.uhn.fhir.model.valueset.BundleTypeEnum; 027import ca.uhn.fhir.rest.annotation.Search; 028import ca.uhn.fhir.rest.api.Constants; 029import ca.uhn.fhir.rest.api.RequestTypeEnum; 030import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 031import ca.uhn.fhir.rest.api.server.IBundleProvider; 032import ca.uhn.fhir.rest.api.server.IRestfulServer; 033import ca.uhn.fhir.rest.api.server.RequestDetails; 034import ca.uhn.fhir.rest.param.ParameterUtil; 035import ca.uhn.fhir.rest.param.QualifierDetails; 036import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 037import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 038import org.apache.commons.lang3.StringUtils; 039import org.hl7.fhir.instance.model.api.IAnyResource; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041 042import javax.annotation.Nonnull; 043import java.lang.reflect.Method; 044import java.util.*; 045 046import static org.apache.commons.lang3.StringUtils.isBlank; 047import static org.apache.commons.lang3.StringUtils.isNotBlank; 048 049public class SearchMethodBinding extends BaseResourceReturningMethodBinding { 050 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class); 051 052 private static final Set<String> SPECIAL_SEARCH_PARAMS; 053 054 static { 055 HashSet<String> specialSearchParams = new HashSet<>(); 056 specialSearchParams.add(IAnyResource.SP_RES_ID); 057 specialSearchParams.add(IAnyResource.SP_RES_LANGUAGE); 058 specialSearchParams.add(Constants.PARAM_INCLUDE); 059 specialSearchParams.add(Constants.PARAM_REVINCLUDE); 060 SPECIAL_SEARCH_PARAMS = Collections.unmodifiableSet(specialSearchParams); 061 } 062 063 private final String myResourceProviderResourceName; 064 private String myCompartmentName; 065 private String myDescription; 066 private Integer myIdParamIndex; 067 private String myQueryName; 068 private boolean myAllowUnknownParams; 069 070 public SearchMethodBinding(Class<? extends IBaseResource> theReturnResourceType, Class<? extends IBaseResource> theResourceProviderResourceType, Method theMethod, FhirContext theContext, Object theProvider) { 071 super(theReturnResourceType, theMethod, theContext, theProvider); 072 Search search = theMethod.getAnnotation(Search.class); 073 this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null); 074 this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null); 075 this.myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); 076 this.myAllowUnknownParams = search.allowUnknownParams(); 077 078 Description desc = theMethod.getAnnotation(Description.class); 079 if (desc != null) { 080 if (isNotBlank(desc.formalDefinition())) { 081 myDescription = StringUtils.defaultIfBlank(desc.formalDefinition(), null); 082 } else { 083 myDescription = StringUtils.defaultIfBlank(desc.shortDefinition(), null); 084 } 085 } 086 087 /* 088 * Only compartment searching methods may have an ID parameter 089 */ 090 if (isBlank(myCompartmentName) && myIdParamIndex != null) { 091 String msg = theContext.getLocalizer().getMessage(getClass().getName() + ".idWithoutCompartment", theMethod.getName(), theMethod.getDeclaringClass()); 092 throw new ConfigurationException(msg); 093 } 094 095 if (theResourceProviderResourceType != null) { 096 this.myResourceProviderResourceName = theContext.getResourceDefinition(theResourceProviderResourceType).getName(); 097 } else { 098 this.myResourceProviderResourceName = null; 099 } 100 101 } 102 103 public String getDescription() { 104 return myDescription; 105 } 106 107 public String getQueryName() { 108 return myQueryName; 109 } 110 111 public String getResourceProviderResourceName() { 112 return myResourceProviderResourceName; 113 } 114 115 @Nonnull 116 @Override 117 public RestOperationTypeEnum getRestOperationType() { 118 return RestOperationTypeEnum.SEARCH_TYPE; 119 } 120 121 @Override 122 protected BundleTypeEnum getResponseBundleType() { 123 return BundleTypeEnum.SEARCHSET; 124 } 125 126 @Override 127 public ReturnTypeEnum getReturnType() { 128 return ReturnTypeEnum.BUNDLE; 129 } 130 131 @Override 132 public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) { 133 134 if (theRequest.getId() != null && myIdParamIndex == null) { 135 ourLog.trace("Method {} doesn't match because ID is not null: {}", getMethod(), theRequest.getId()); 136 return false; 137 } 138 if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { 139 ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation()); 140 return false; 141 } 142 if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { 143 ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation()); 144 return false; 145 } 146 if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) { 147 ourLog.trace("Method {} doesn't match because request type is {}", getMethod(), theRequest.getRequestType()); 148 return false; 149 } 150 if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) { 151 ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", getMethod(), myCompartmentName, theRequest.getCompartmentName()); 152 return false; 153 } 154 if (theRequest.getParameters().get(Constants.PARAM_PAGINGACTION) != null) { 155 return false; 156 } 157 158 // This is used to track all the parameters so we can reject queries that 159 // have additional params we don't understand 160 Set<String> methodParamsTemp = new HashSet<>(); 161 162 Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet(); 163 Set<String> qualifiedParamNames = theRequest.getParameters().keySet(); 164 for (IParameter nextParameter : getParameters()) { 165 if (!(nextParameter instanceof BaseQueryParameter)) { 166 continue; 167 } 168 BaseQueryParameter nextQueryParameter = (BaseQueryParameter) nextParameter; 169 String name = nextQueryParameter.getName(); 170 if (nextQueryParameter.isRequired()) { 171 172 if (qualifiedParamNames.contains(name)) { 173 QualifierDetails qualifiers = extractQualifiersFromParameterName(name); 174 if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) { 175 methodParamsTemp.add(name); 176 } 177 } 178 if (unqualifiedNames.contains(name)) { 179 List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name); 180 qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist()); 181 methodParamsTemp.addAll(qualifiedNames); 182 } 183 if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name)) { 184 ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name); 185 return false; 186 } 187 188 } else { 189 if (qualifiedParamNames.contains(name)) { 190 QualifierDetails qualifiers = extractQualifiersFromParameterName(name); 191 if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) { 192 methodParamsTemp.add(name); 193 } 194 } 195 if (unqualifiedNames.contains(name)) { 196 List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name); 197 qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist()); 198 methodParamsTemp.addAll(qualifiedNames); 199 } 200 if (!qualifiedParamNames.contains(name)) { 201 methodParamsTemp.add(name); 202 } 203 } 204 } 205 if (myQueryName != null) { 206 String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY); 207 if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) { 208 String queryName = queryNameValues[0]; 209 if (!myQueryName.equals(queryName)) { 210 ourLog.trace("Query name does not match {}", myQueryName); 211 return false; 212 } 213 methodParamsTemp.add(Constants.PARAM_QUERY); 214 } else { 215 ourLog.trace("Query name does not match {}", myQueryName); 216 return false; 217 } 218 } else { 219 String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY); 220 if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) { 221 ourLog.trace("Query has name"); 222 return false; 223 } 224 } 225 for (String next : theRequest.getParameters().keySet()) { 226 if (next.startsWith("_") && !SPECIAL_SEARCH_PARAMS.contains(truncModifierPart(next))) { 227 methodParamsTemp.add(next); 228 } 229 } 230 Set<String> keySet = theRequest.getParameters().keySet(); 231 232 if (myAllowUnknownParams == false) { 233 for (String next : keySet) { 234 if (!methodParamsTemp.contains(next)) { 235 return false; 236 } 237 } 238 } 239 return true; 240 } 241 242 private String truncModifierPart(String param) { 243 int indexOfSeparator = param.indexOf(":"); 244 if (indexOfSeparator != -1) { 245 return param.substring(0, indexOfSeparator); 246 } 247 return param; 248 } 249 250 @Override 251 public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { 252 if (myIdParamIndex != null) { 253 theMethodParams[myIdParamIndex] = theRequest.getId(); 254 } 255 256 Object response = invokeServerMethod(theServer, theRequest, theMethodParams); 257 258 return toResourceList(response); 259 260 } 261 262 @Override 263 protected boolean isAddContentLocationHeader() { 264 return false; 265 } 266 267 private List<String> processWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) { 268 if (theQualifierWhitelist == null && theQualifierBlacklist == null) { 269 return theQualifiedNames; 270 } 271 ArrayList<String> retVal = new ArrayList<>(theQualifiedNames.size()); 272 for (String next : theQualifiedNames) { 273 QualifierDetails qualifiers = extractQualifiersFromParameterName(next); 274 if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) { 275 continue; 276 } 277 retVal.add(next); 278 } 279 return retVal; 280 } 281 282 @Override 283 public String toString() { 284 return getMethod().toString(); 285 } 286 287 public static QualifierDetails extractQualifiersFromParameterName(String theParamName) { 288 QualifierDetails retVal = new QualifierDetails(); 289 if (theParamName == null || theParamName.length() == 0) { 290 return retVal; 291 } 292 293 int dotIdx = -1; 294 int colonIdx = -1; 295 for (int idx = 0; idx < theParamName.length(); idx++) { 296 char nextChar = theParamName.charAt(idx); 297 if (nextChar == '.' && dotIdx == -1) { 298 dotIdx = idx; 299 } else if (nextChar == ':' && colonIdx == -1) { 300 colonIdx = idx; 301 } 302 } 303 304 if (dotIdx != -1 && colonIdx != -1) { 305 if (dotIdx < colonIdx) { 306 retVal.setDotQualifier(theParamName.substring(dotIdx, colonIdx)); 307 retVal.setColonQualifier(theParamName.substring(colonIdx)); 308 retVal.setParamName(theParamName.substring(0, dotIdx)); 309 retVal.setWholeQualifier(theParamName.substring(dotIdx)); 310 } else { 311 retVal.setColonQualifier(theParamName.substring(colonIdx, dotIdx)); 312 retVal.setDotQualifier(theParamName.substring(dotIdx)); 313 retVal.setParamName(theParamName.substring(0, colonIdx)); 314 retVal.setWholeQualifier(theParamName.substring(colonIdx)); 315 } 316 } else if (dotIdx != -1) { 317 retVal.setDotQualifier(theParamName.substring(dotIdx)); 318 retVal.setParamName(theParamName.substring(0, dotIdx)); 319 retVal.setWholeQualifier(theParamName.substring(dotIdx)); 320 } else if (colonIdx != -1) { 321 retVal.setColonQualifier(theParamName.substring(colonIdx)); 322 retVal.setParamName(theParamName.substring(0, colonIdx)); 323 retVal.setWholeQualifier(theParamName.substring(colonIdx)); 324 } else { 325 retVal.setParamName(theParamName); 326 retVal.setColonQualifier(null); 327 retVal.setDotQualifier(null); 328 retVal.setWholeQualifier(null); 329 } 330 331 return retVal; 332 } 333 334 335}