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