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}