001package ca.uhn.fhir.rest.client.method; 002 003/* 004 * #%L 005 * HAPI FHIR - Client 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 */ 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.RestOperationTypeEnum; 030import ca.uhn.fhir.rest.api.SearchStyleEnum; 031import ca.uhn.fhir.rest.client.api.UrlSourceEnum; 032import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; 033import ca.uhn.fhir.rest.param.ParameterUtil; 034import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 035import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 036import ca.uhn.fhir.util.ParametersUtil; 037import org.apache.commons.lang3.StringUtils; 038import org.hl7.fhir.instance.model.api.IBaseResource; 039import org.hl7.fhir.instance.model.api.IIdType; 040 041import java.lang.reflect.Method; 042import java.util.Collections; 043import java.util.LinkedHashMap; 044import java.util.List; 045import java.util.Map; 046import java.util.Map.Entry; 047 048import static org.apache.commons.lang3.StringUtils.isBlank; 049import static org.apache.commons.lang3.StringUtils.isNotBlank; 050 051public class SearchMethodBinding extends BaseResourceReturningMethodBinding { 052 private String myCompartmentName; 053 private String myDescription; 054 private Integer myIdParamIndex; 055 private String myQueryName; 056 057 public SearchMethodBinding(Class<? extends IBaseResource> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { 058 super(theReturnResourceType, theMethod, theContext, theProvider); 059 Search search = theMethod.getAnnotation(Search.class); 060 this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null); 061 this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null); 062 this.myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); 063 this.myDescription = ParametersUtil.extractDescription(theMethod); 064 065 /* 066 * Check for parameter combinations and names that are invalid 067 */ 068 List<IParameter> parameters = getParameters(); 069 // List<SearchParameter> searchParameters = new ArrayList<SearchParameter>(); 070 for (int i = 0; i < parameters.size(); i++) { 071 IParameter next = parameters.get(i); 072 if (!(next instanceof SearchParameter)) { 073 continue; 074 } 075 076 SearchParameter sp = (SearchParameter) next; 077 if (sp.getName().startsWith("_")) { 078 if (ALLOWED_PARAMS.contains(sp.getName())) { 079 String msg = getContext().getLocalizer().getMessage(getClass().getName() + ".invalidSpecialParamName", theMethod.getName(), theMethod.getDeclaringClass().getSimpleName(), 080 sp.getName()); 081 throw new ConfigurationException(msg); 082 } 083 } 084 085 // searchParameters.add(sp); 086 } 087 // for (int i = 0; i < searchParameters.size(); i++) { 088 // SearchParameter next = searchParameters.get(i); 089 // // next. 090 // } 091 092 /* 093 * Only compartment searching methods may have an ID parameter 094 */ 095 if (isBlank(myCompartmentName) && myIdParamIndex != null) { 096 String msg = theContext.getLocalizer().getMessage(getClass().getName() + ".idWithoutCompartment", theMethod.getName(), theMethod.getDeclaringClass()); 097 throw new ConfigurationException(msg); 098 } 099 100 } 101 102 public String getDescription() { 103 return myDescription; 104 } 105 106 @Override 107 protected BundleTypeEnum getResponseBundleType() { 108 return BundleTypeEnum.SEARCHSET; 109 } 110 111 @Override 112 public RestOperationTypeEnum getRestOperationType() { 113 return RestOperationTypeEnum.SEARCH_TYPE; 114 } 115 116 @Override 117 public ReturnTypeEnum getReturnType() { 118 return ReturnTypeEnum.BUNDLE; 119 } 120 121 122 @Override 123 public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { 124 assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == getParameters().size())) : "Wrong number of arguments: " + (theArgs != null ? theArgs.length : "null"); 125 126 Map<String, List<String>> queryStringArgs = new LinkedHashMap<String, List<String>>(); 127 128 if (myQueryName != null) { 129 queryStringArgs.put(Constants.PARAM_QUERY, Collections.singletonList(myQueryName)); 130 } 131 132 IIdType id = (IIdType) (myIdParamIndex != null ? theArgs[myIdParamIndex] : null); 133 134 String resourceName = getResourceName(); 135 if (theArgs != null) { 136 for (int idx = 0; idx < theArgs.length; idx++) { 137 IParameter nextParam = getParameters().get(idx); 138 nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], queryStringArgs, null); 139 } 140 } 141 142 BaseHttpClientInvocation retVal = createSearchInvocation(getContext(), resourceName, queryStringArgs, id, myCompartmentName, null); 143 144 return retVal; 145 } 146 147 148 @Override 149 protected boolean isAddContentLocationHeader() { 150 return false; 151 } 152 153 154 @Override 155 public String toString() { 156 return getMethod().toString(); 157 } 158 159 public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theSearchUrl, UrlSourceEnum theUrlSource, Map<String, List<String>> theParams) { 160 return new HttpGetClientInvocation(theContext, theParams, theUrlSource, theSearchUrl); 161 } 162 163 164 public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map<String, List<String>> theParameters, IIdType theId, String theCompartmentName, 165 SearchStyleEnum theSearchStyle) { 166 SearchStyleEnum searchStyle = theSearchStyle; 167 if (searchStyle == null) { 168 int length = 0; 169 for (Entry<String, List<String>> nextEntry : theParameters.entrySet()) { 170 length += nextEntry.getKey().length(); 171 for (String next : nextEntry.getValue()) { 172 length += next.length(); 173 } 174 } 175 176 if (length < 5000) { 177 searchStyle = SearchStyleEnum.GET; 178 } else { 179 searchStyle = SearchStyleEnum.POST; 180 } 181 } 182 183 BaseHttpClientInvocation invocation; 184 185 boolean compartmentSearch = false; 186 if (theCompartmentName != null) { 187 if (theId == null || !theId.hasIdPart()) { 188 String msg = theContext.getLocalizer().getMessage(SearchMethodBinding.class.getName() + ".idNullForCompartmentSearch"); 189 throw new InvalidRequestException(msg); 190 } 191 compartmentSearch = true; 192 } 193 194 /* 195 * Are we doing a get (GET [base]/Patient?name=foo) or a get with search (GET [base]/Patient/_search?name=foo) or a post (POST [base]/Patient with parameters in the POST body) 196 */ 197 switch (searchStyle) { 198 case GET: 199 default: 200 if (compartmentSearch) { 201 invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName); 202 } else { 203 invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName); 204 } 205 break; 206 case GET_WITH_SEARCH: 207 if (compartmentSearch) { 208 invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH); 209 } else { 210 invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH); 211 } 212 break; 213 case POST: 214 if (compartmentSearch) { 215 invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH); 216 } else { 217 invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH); 218 } 219 } 220 221 return invocation; 222 } 223 224}