001package ca.uhn.fhir.rest.method;
002
003import static org.apache.commons.lang3.StringUtils.isBlank;
004import static org.apache.commons.lang3.StringUtils.isNotBlank;
005
006import java.io.IOException;
007import java.io.PushbackReader;
008import java.io.Reader;
009import java.lang.annotation.Annotation;
010import java.lang.reflect.Method;
011import java.util.*;
012import java.util.Map.Entry;
013
014import org.apache.commons.lang3.ObjectUtils;
015import org.apache.commons.lang3.StringUtils;
016import org.hl7.fhir.instance.model.api.*;
017
018import ca.uhn.fhir.context.*;
019import ca.uhn.fhir.model.api.*;
020import ca.uhn.fhir.model.api.annotation.Description;
021import ca.uhn.fhir.model.primitive.IdDt;
022import ca.uhn.fhir.model.primitive.InstantDt;
023import ca.uhn.fhir.parser.IParser;
024import ca.uhn.fhir.rest.annotation.*;
025import ca.uhn.fhir.rest.api.*;
026import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
027import ca.uhn.fhir.rest.method.OperationParameter.IOperationParamConverter;
028import ca.uhn.fhir.rest.param.*;
029import ca.uhn.fhir.rest.param.ResourceParameter.Mode;
030import ca.uhn.fhir.rest.server.Constants;
031import ca.uhn.fhir.rest.server.EncodingEnum;
032import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider;
033import ca.uhn.fhir.rest.server.SearchParameterMap;
034import ca.uhn.fhir.util.DateUtils;
035import ca.uhn.fhir.util.ParametersUtil;
036import ca.uhn.fhir.util.ReflectionUtil;
037import ca.uhn.fhir.util.UrlUtil;
038
039/*
040 * #%L
041 * HAPI FHIR - Core Library
042 * %%
043 * Copyright (C) 2014 - 2017 University Health Network
044 * %%
045 * Licensed under the Apache License, Version 2.0 (the "License");
046 * you may not use this file except in compliance with the License.
047 * You may obtain a copy of the License at
048 * 
049 *      http://www.apache.org/licenses/LICENSE-2.0
050 * 
051 * Unless required by applicable law or agreed to in writing, software
052 * distributed under the License is distributed on an "AS IS" BASIS,
053 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
054 * See the License for the specific language governing permissions and
055 * limitations under the License.
056 * #L%
057 */
058
059@SuppressWarnings("deprecation")
060public class MethodUtil {
061        
062        private static final String LABEL = "label=\"";
063        
064        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MethodUtil.class);
065        private static final Set<String> ourServletRequestTypes = new HashSet<String>();
066        private static final Set<String> ourServletResponseTypes = new HashSet<String>();
067        private static final String SCHEME = "scheme=\"";
068        static {
069                ourServletRequestTypes.add("javax.servlet.ServletRequest");
070                ourServletResponseTypes.add("javax.servlet.ServletResponse");
071                ourServletRequestTypes.add("javax.servlet.http.HttpServletRequest");
072                ourServletResponseTypes.add("javax.servlet.http.HttpServletResponse");
073        }
074        
075        /** Non instantiable */
076        private MethodUtil() {
077                // nothing
078        }
079        
080
081        static void addTagsToPostOrPut(FhirContext theContext, IBaseResource resource, BaseHttpClientInvocation retVal) {
082                if (theContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU1)) {
083                        TagList list = (TagList) ((IResource)resource).getResourceMetadata().get(ResourceMetadataKeyEnum.TAG_LIST);
084                        if (list != null) {
085                                for (Tag tag : list) {
086                                        if (StringUtils.isNotBlank(tag.getTerm())) {
087                                                retVal.addHeader(Constants.HEADER_CATEGORY, tag.toHeaderValue());
088                                        }
089                                }
090                        }
091                }
092        }
093
094        
095        @SuppressWarnings("unchecked")
096        public static <T extends IIdType> T convertIdToType(IIdType value, Class<T> theIdParamType) {
097                if (value != null && !theIdParamType.isAssignableFrom(value.getClass())) {
098                        IIdType newValue = ReflectionUtil.newInstance(theIdParamType);
099                        newValue.setValue(value.getValue());
100                        value = newValue;
101                }
102                return (T) value;
103        }
104
105        public static HttpGetClientInvocation createConformanceInvocation(FhirContext theContext) {
106                return new HttpGetClientInvocation(theContext, "metadata");
107        }
108
109        public static HttpPostClientInvocation createCreateInvocation(IBaseResource theResource, FhirContext theContext) {
110                return createCreateInvocation(theResource, null, null, theContext);
111        }
112
113        public static HttpPostClientInvocation createCreateInvocation(IBaseResource theResource, String theResourceBody, String theId, FhirContext theContext) {
114                RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource);
115                String resourceName = def.getName();
116
117                StringBuilder urlExtension = new StringBuilder();
118                urlExtension.append(resourceName);
119
120                boolean dstu1 = theContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU1);
121                if (dstu1) {
122                        /*
123                         * This was allowable at one point, but as of DSTU2 it isn't.
124                         */
125                        if (StringUtils.isNotBlank(theId)) {
126                                urlExtension.append('/');
127                                urlExtension.append(theId);
128                        }
129                }
130                
131                HttpPostClientInvocation retVal;
132                if (StringUtils.isBlank(theResourceBody)) {
133                        retVal = new HttpPostClientInvocation(theContext, theResource, urlExtension.toString());
134                } else {
135                        retVal = new HttpPostClientInvocation(theContext, theResourceBody, false, urlExtension.toString());
136                }
137                addTagsToPostOrPut(theContext, theResource, retVal);
138
139                if (!dstu1) {
140                        retVal.setOmitResourceId(true);
141                }
142                // addContentTypeHeaderBasedOnDetectedType(retVal, theResourceBody);
143
144                return retVal;
145        }
146
147        public static HttpPostClientInvocation createCreateInvocation(IBaseResource theResource, String theResourceBody, String theId, FhirContext theContext, Map<String, List<String>> theIfNoneExistParams) {
148                HttpPostClientInvocation retVal = createCreateInvocation(theResource, theResourceBody, theId, theContext);
149                retVal.setIfNoneExistParams(theIfNoneExistParams);
150                return retVal;
151        }
152
153        public static HttpPostClientInvocation createCreateInvocation(IBaseResource theResource, String theResourceBody, String theId, FhirContext theContext, String theIfNoneExistUrl) {
154                HttpPostClientInvocation retVal = createCreateInvocation(theResource, theResourceBody, theId, theContext);
155                retVal.setIfNoneExistString(theIfNoneExistUrl);
156                return retVal;
157        }
158        
159        public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, IIdType theId, PatchTypeEnum thePatchType, String theBody) {
160                return PatchMethodBinding.createPatchInvocation(theContext, theId, thePatchType, theBody);
161        }
162        
163        public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, String theUrl, PatchTypeEnum thePatchType, String theBody) {
164                return PatchMethodBinding.createPatchInvocation(theContext, theUrl, thePatchType, theBody);
165        }
166
167        public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, PatchTypeEnum thePatchType, String theBody, String theResourceType, Map<String, List<String>> theMatchParams) {
168                return PatchMethodBinding.createPatchInvocation(theContext, thePatchType, theBody, theResourceType, theMatchParams);
169        }
170
171        public static HttpPutClientInvocation createUpdateInvocation(FhirContext theContext, IBaseResource theResource, String theResourceBody, Map<String, List<String>> theMatchParams) {
172                String resourceType = theContext.getResourceDefinition(theResource).getName();
173
174                StringBuilder b = createUrl(resourceType, theMatchParams);
175
176                HttpPutClientInvocation retVal;
177                if (StringUtils.isBlank(theResourceBody)) {
178                        retVal = new HttpPutClientInvocation(theContext, theResource, b.toString());
179                } else {
180                        retVal = new HttpPutClientInvocation(theContext, theResourceBody, false, b.toString());
181                }
182
183                addTagsToPostOrPut(theContext, theResource, retVal);
184
185                return retVal;
186        }
187
188
189        public static StringBuilder createUrl(String theResourceType, Map<String, List<String>> theMatchParams) {
190                StringBuilder b = new StringBuilder();
191
192                b.append(theResourceType);
193
194                boolean haveQuestionMark = false;
195                for (Entry<String, List<String>> nextEntry : theMatchParams.entrySet()) {
196                        for (String nextValue : nextEntry.getValue()) {
197                                b.append(haveQuestionMark ? '&' : '?');
198                                haveQuestionMark = true;
199                                b.append(UrlUtil.escape(nextEntry.getKey()));
200                                b.append('=');
201                                b.append(UrlUtil.escape(nextValue));
202                        }
203                }
204                return b;
205        }
206
207        
208        public static HttpPutClientInvocation createUpdateInvocation(FhirContext theContext, IBaseResource theResource, String theResourceBody, String theMatchUrl) {
209                HttpPutClientInvocation retVal;
210                if (StringUtils.isBlank(theResourceBody)) {
211                        retVal = new HttpPutClientInvocation(theContext, theResource, theMatchUrl);
212                } else {
213                        retVal = new HttpPutClientInvocation(theContext, theResourceBody, false, theMatchUrl);
214                }
215
216                addTagsToPostOrPut(theContext, theResource, retVal);
217
218                return retVal;
219        }
220
221        public static HttpPutClientInvocation createUpdateInvocation(IBaseResource theResource, String theResourceBody, IIdType theId, FhirContext theContext) {
222                String resourceName = theContext.getResourceDefinition(theResource).getName();
223                StringBuilder urlBuilder = new StringBuilder();
224                urlBuilder.append(resourceName);
225                urlBuilder.append('/');
226                urlBuilder.append(theId.getIdPart());
227                String urlExtension = urlBuilder.toString();
228
229                HttpPutClientInvocation retVal;
230                if (StringUtils.isBlank(theResourceBody)) {
231                        retVal = new HttpPutClientInvocation(theContext, theResource, urlExtension);
232                } else {
233                        retVal = new HttpPutClientInvocation(theContext, theResourceBody, false, urlExtension);
234                }
235                
236                retVal.setForceResourceId(theId);
237
238                if (theId.hasVersionIdPart()) {
239                        if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU1)) {
240                                retVal.addHeader(Constants.HEADER_IF_MATCH, '"' + theId.getVersionIdPart() + '"');
241                        } else {
242                                String versionId = theId.getVersionIdPart();
243                                if (StringUtils.isNotBlank(versionId)) {
244                                        urlBuilder.append('/');
245                                        urlBuilder.append(Constants.PARAM_HISTORY);
246                                        urlBuilder.append('/');
247                                        urlBuilder.append(versionId);
248                                        retVal.addHeader(Constants.HEADER_CONTENT_LOCATION, urlBuilder.toString());
249                                }
250                        }
251                }
252
253                addTagsToPostOrPut(theContext, theResource, retVal);
254                // addContentTypeHeaderBasedOnDetectedType(retVal, theResourceBody);
255
256                return retVal;
257        }
258
259        public static EncodingEnum detectEncoding(String theBody) {
260                EncodingEnum retVal = detectEncodingNoDefault(theBody);
261                retVal = ObjectUtils.defaultIfNull(retVal, EncodingEnum.XML);
262                return retVal;
263        }
264
265        public static EncodingEnum detectEncodingNoDefault(String theBody) {
266                EncodingEnum retVal = null;
267                for (int i = 0; i < theBody.length() && retVal == null; i++) {
268                        switch (theBody.charAt(i)) {
269                                case '<':
270                                        retVal = EncodingEnum.XML;
271                                        break;
272                                case '{':
273                                        retVal = EncodingEnum.JSON;
274                                        break;
275                        }
276                }
277                return retVal;
278        }
279
280        public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) {
281                for (Annotation annotation : theAnnotations) {
282                        if (annotation instanceof Description) {
283                                Description desc = (Description) annotation;
284                                if (isNotBlank(desc.formalDefinition())) {
285                                        theParameter.setDescription(desc.formalDefinition());
286                                } else {
287                                        theParameter.setDescription(desc.shortDefinition());
288                                }
289                        }
290                }
291        }
292
293        public static Integer findIdParameterIndex(Method theMethod, FhirContext theContext) {
294                Integer index = MethodUtil.findParamAnnotationIndex(theMethod, IdParam.class);
295                if (index != null) {
296                        Class<?> paramType = theMethod.getParameterTypes()[index];
297                        if (IIdType.class.equals(paramType)) {
298                                return index;
299                        }
300                        boolean isRi = theContext.getVersion().getVersion().isRi();
301                        boolean usesHapiId = IdDt.class.equals(paramType);
302                        if (isRi == usesHapiId) {
303                                throw new ConfigurationException("Method uses the wrong Id datatype (IdDt / IdType) for the given context FHIR version: " + theMethod.toString());
304                        }
305                }
306                return index;
307        }
308
309        @SuppressWarnings("GetClassOnAnnotation")
310        public static Integer findParamAnnotationIndex(Method theMethod, Class<?> toFind) {
311                int paramIndex = 0;
312                for (Annotation[] annotations : theMethod.getParameterAnnotations()) {
313                        for (int annotationIndex = 0; annotationIndex < annotations.length; annotationIndex++) {
314                                Annotation nextAnnotation = annotations[annotationIndex];
315                                Class<? extends Annotation> class1 = nextAnnotation.getClass();
316                                if (toFind.isAssignableFrom(class1)) {
317                                        return paramIndex;
318                                }
319                        }
320                        paramIndex++;
321                }
322                return null;
323        }
324
325        public static Integer findTagListParameterIndex(Method theMethod) {
326                return MethodUtil.findParamAnnotationIndex(theMethod, TagListParam.class);
327        }
328
329        public static Integer findVersionIdParameterIndex(Method theMethod) {
330                return MethodUtil.findParamAnnotationIndex(theMethod, VersionIdParam.class);
331        }
332
333        @SuppressWarnings("unchecked")
334        public static List<IParameter> getResourceParameters(final FhirContext theContext, Method theMethod, Object theProvider, RestOperationTypeEnum theRestfulOperationTypeEnum) {
335                List<IParameter> parameters = new ArrayList<IParameter>();
336
337                Class<?>[] parameterTypes = theMethod.getParameterTypes();
338                int paramIndex = 0;
339                for (Annotation[] annotations : theMethod.getParameterAnnotations()) {
340
341                        IParameter param = null;
342                        Class<?> parameterType = parameterTypes[paramIndex];
343                        Class<? extends java.util.Collection<?>> outerCollectionType = null;
344                        Class<? extends java.util.Collection<?>> innerCollectionType = null;
345                        if (SearchParameterMap.class.equals(parameterType)) {
346                                if (theProvider instanceof IDynamicSearchResourceProvider) {
347                                        Search searchAnnotation = theMethod.getAnnotation(Search.class);
348                                        if (searchAnnotation != null && searchAnnotation.dynamic()) {
349                                                param = new DynamicSearchParameter((IDynamicSearchResourceProvider) theProvider);
350                                        }
351                                }
352                        } else if (TagList.class.isAssignableFrom(parameterType)) {
353                                // TagList is handled directly within the method bindings
354                                param = new NullParameter();
355                        } else {
356                                if (Collection.class.isAssignableFrom(parameterType)) {
357                                        innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType;
358                                        parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
359                                }
360                                if (Collection.class.isAssignableFrom(parameterType)) {
361                                        outerCollectionType = innerCollectionType;
362                                        innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType;
363                                        parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
364                                }
365                                if (Collection.class.isAssignableFrom(parameterType)) {
366                                        throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() + "' is of an invalid generic type (can not be a collection of a collection of a collection)");
367                                }
368                        }
369                        
370                        /* 
371                         * Note: for the first two here, we're using strings instead of static binding
372                         * so that we don't need the java.servlet JAR on the classpath in order to use
373                         * this class 
374                         */
375                        if (ourServletRequestTypes.contains(parameterType.getName())) {
376                                param = new ServletRequestParameter();
377                        } else if (ourServletResponseTypes.contains(parameterType.getName())) {
378                                param = new ServletResponseParameter();
379                        } else if (parameterType.equals(RequestDetails.class)) {
380                                param = new RequestDetailsParameter();
381                        } else if (parameterType.equals(IRequestOperationCallback.class)) {
382                                param = new RequestOperationCallbackParameter();
383                        } else if (parameterType.equals(SummaryEnum.class)) {
384                                param = new SummaryEnumParameter();
385                        } else if (parameterType.equals(PatchTypeEnum.class)) {
386                                param = new PatchTypeParameter();
387                        } else {
388                                for (int i = 0; i < annotations.length && param == null; i++) {
389                                        Annotation nextAnnotation = annotations[i];
390
391                                        if (nextAnnotation instanceof RequiredParam) {
392                                                SearchParameter parameter = new SearchParameter();
393                                                parameter.setName(((RequiredParam) nextAnnotation).name());
394                                                parameter.setRequired(true);
395                                                parameter.setDeclaredTypes(((RequiredParam) nextAnnotation).targetTypes());
396                                                parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes());
397                                                parameter.setChainlists(((RequiredParam) nextAnnotation).chainWhitelist(), ((RequiredParam) nextAnnotation).chainBlacklist());
398                                                parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
399                                                MethodUtil.extractDescription(parameter, annotations);
400                                                param = parameter;
401                                        } else if (nextAnnotation instanceof OptionalParam) {
402                                                SearchParameter parameter = new SearchParameter();
403                                                parameter.setName(((OptionalParam) nextAnnotation).name());
404                                                parameter.setRequired(false);
405                                                parameter.setDeclaredTypes(((OptionalParam) nextAnnotation).targetTypes());
406                                                parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes());
407                                                parameter.setChainlists(((OptionalParam) nextAnnotation).chainWhitelist(), ((OptionalParam) nextAnnotation).chainBlacklist());
408                                                parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType);
409                                                MethodUtil.extractDescription(parameter, annotations);
410                                                param = parameter;
411                                        } else if (nextAnnotation instanceof RawParam) {
412                                                param = new RawParamsParmeter(parameters);
413                                        } else if (nextAnnotation instanceof IncludeParam) {
414                                                Class<? extends Collection<Include>> instantiableCollectionType;
415                                                Class<?> specType;
416
417                                                if (parameterType == String.class) {
418                                                        instantiableCollectionType = null;
419                                                        specType = String.class;
420                                                } else if ((parameterType != Include.class) || innerCollectionType == null || outerCollectionType != null) {
421                                                        throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" + Include.class.getSimpleName() + ">");
422                                                } else {
423                                                        instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'");
424                                                        specType = parameterType;
425                                                }
426
427                                                param = new IncludeParameter((IncludeParam) nextAnnotation, instantiableCollectionType, specType);
428                                        } else if (nextAnnotation instanceof ResourceParam) {
429                                                Mode mode;
430                                                if (IBaseResource.class.isAssignableFrom(parameterType)) {
431                                                        mode = Mode.RESOURCE;
432                                                } else if (String.class.equals(parameterType)) {
433                                                        mode = ResourceParameter.Mode.BODY;
434                                                } else if (byte[].class.equals(parameterType)) {
435                                                        mode = ResourceParameter.Mode.BODY_BYTE_ARRAY;
436                                                } else if (EncodingEnum.class.equals(parameterType)) {
437                                                        mode = Mode.ENCODING;
438                                                } else {
439                                                        StringBuilder b = new StringBuilder();
440                                                        b.append("Method '");
441                                                        b.append(theMethod.getName());
442                                                        b.append("' is annotated with @");
443                                                        b.append(ResourceParam.class.getSimpleName());
444                                                        b.append(" but has a type that is not an implemtation of ");
445                                                        b.append(IBaseResource.class.getCanonicalName());
446                                                        b.append(" or String or byte[]");
447                                                        throw new ConfigurationException(b.toString());
448                                                }
449                                                param = new ResourceParameter((Class<? extends IResource>) parameterType, theProvider, mode);
450                                        } else if (nextAnnotation instanceof IdParam || nextAnnotation instanceof VersionIdParam) {
451                                                param = new NullParameter();
452                                        } else if (nextAnnotation instanceof ServerBase) {
453                                                param = new ServerBaseParamBinder();
454                                        } else if (nextAnnotation instanceof Elements) {
455                                                param = new ElementsParameter();
456                                        } else if (nextAnnotation instanceof Since) {
457                                                param = new SinceParameter();
458                                                ((SinceParameter)param).setType(theContext, parameterType, innerCollectionType, outerCollectionType);
459                                        } else if (nextAnnotation instanceof At) {
460                                                param = new AtParameter();
461                                                ((AtParameter)param).setType(theContext, parameterType, innerCollectionType, outerCollectionType);
462                                        } else if (nextAnnotation instanceof Count) {
463                                                param = new CountParameter();
464                                        } else if (nextAnnotation instanceof Sort) {
465                                                param = new SortParameter(theContext);
466                                        } else if (nextAnnotation instanceof TransactionParam) {
467                                                param = new TransactionParameter(theContext);
468                                        } else if (nextAnnotation instanceof ConditionalUrlParam) {
469                                                param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam)nextAnnotation).supportsMultiple());
470                                        } else if (nextAnnotation instanceof OperationParam) {
471                                                Operation op = theMethod.getAnnotation(Operation.class);
472                                                param = new OperationParameter(theContext, op.name(), ((OperationParam) nextAnnotation));
473                                        } else if (nextAnnotation instanceof Validate.Mode) {
474                                                if (parameterType.equals(ValidationModeEnum.class) == false) {
475                                                        throw new ConfigurationException("Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName());
476                                                }
477                                                param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() {
478                                                        @Override
479                                                        public Object incomingServer(Object theObject) {
480                                                                if (isNotBlank(theObject.toString())) {
481                                                                        ValidationModeEnum retVal = ValidationModeEnum.forCode(theObject.toString());
482                                                                        if (retVal == null) {
483                                                                                OperationParameter.throwInvalidMode(theObject.toString());
484                                                                        }
485                                                                        return retVal;
486                                                                }
487                                                                return null;
488                                                        }
489                                                        
490                                                        @Override
491                                                        public Object outgoingClient(Object theObject) {
492                                                                return ParametersUtil.createString(theContext, ((ValidationModeEnum)theObject).getCode());
493                                                        }
494                                                });
495                                        } else if (nextAnnotation instanceof Validate.Profile) {
496                                                if (parameterType.equals(String.class) == false) {
497                                                        throw new ConfigurationException("Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName());
498                                                }
499                                                param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() {
500                                                        @Override
501                                                        public Object incomingServer(Object theObject) {
502                                                                return theObject.toString();
503                                                        }
504                                                        
505                                                        @Override
506                                                        public Object outgoingClient(Object theObject) {
507                                                                return ParametersUtil.createString(theContext, theObject.toString());
508                                                        }
509                                                });
510                                        } else {
511                                                continue;
512                                        }
513
514                                }
515
516                        }
517
518                        if (param == null) {
519                                throw new ConfigurationException("Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName()
520                                                + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter");
521                        }
522
523                        param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType);
524                        parameters.add(param);
525
526                        paramIndex++;
527                }
528                return parameters;
529        }
530
531        public static void parseClientRequestResourceHeaders(IIdType theRequestedId, Map<String, List<String>> theHeaders, IBaseResource resource) {
532                List<String> lmHeaders = theHeaders.get(Constants.HEADER_LAST_MODIFIED_LOWERCASE);
533                if (lmHeaders != null && lmHeaders.size() > 0 && StringUtils.isNotBlank(lmHeaders.get(0))) {
534                        String headerValue = lmHeaders.get(0);
535                        Date headerDateValue;
536                        try {
537                                headerDateValue = DateUtils.parseDate(headerValue);
538                                if (resource instanceof IResource) {
539                                        IResource iResource = (IResource) resource;
540                                        InstantDt existing = ResourceMetadataKeyEnum.UPDATED.get(iResource);
541                                        if (existing == null || existing.isEmpty()) {
542                                                InstantDt lmValue = new InstantDt(headerDateValue);
543                                                iResource.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, lmValue);
544                                        }
545                                } else if (resource instanceof IAnyResource) {
546                                        IAnyResource anyResource = (IAnyResource) resource;
547                                        if (anyResource.getMeta().getLastUpdated() == null) {
548                                                anyResource.getMeta().setLastUpdated(headerDateValue);
549                                        }
550                                }
551                        } catch (Exception e) {
552                                ourLog.warn("Unable to parse date string '{}'. Error is: {}", headerValue, e.toString());
553                        }
554                }
555
556                List<String> clHeaders = theHeaders.get(Constants.HEADER_CONTENT_LOCATION_LC);
557                if (clHeaders != null && clHeaders.size() > 0 && StringUtils.isNotBlank(clHeaders.get(0))) {
558                        String headerValue = clHeaders.get(0);
559                        if (isNotBlank(headerValue)) {
560                                new IdDt(headerValue).applyTo(resource);
561                        }
562                }
563
564                List<String> locationHeaders = theHeaders.get(Constants.HEADER_LOCATION_LC);
565                if (locationHeaders != null && locationHeaders.size() > 0 && StringUtils.isNotBlank(locationHeaders.get(0))) {
566                        String headerValue = locationHeaders.get(0);
567                        if (isNotBlank(headerValue)) {
568                                new IdDt(headerValue).applyTo(resource);
569                        }
570                }
571
572                IdDt existing = IdDt.of(resource);
573
574                List<String> eTagHeaders = theHeaders.get(Constants.HEADER_ETAG_LC);
575                String eTagVersion = null;
576                if (eTagHeaders != null && eTagHeaders.size() > 0) {
577                        eTagVersion = parseETagValue(eTagHeaders.get(0));
578                }
579                if (isNotBlank(eTagVersion)) {
580                        if (existing == null || existing.isEmpty()) {
581                                if (theRequestedId != null) {
582                                        theRequestedId.withVersion(eTagVersion).applyTo(resource);
583                                }
584                        } else if (existing.hasVersionIdPart() == false) {
585                                existing.withVersion(eTagVersion).applyTo(resource);
586                        }
587                } else if (existing == null || existing.isEmpty()) {
588                        if (theRequestedId != null) {
589                                theRequestedId.applyTo(resource);
590                        }
591                }
592
593                List<String> categoryHeaders = theHeaders.get(Constants.HEADER_CATEGORY_LC);
594                if (categoryHeaders != null && categoryHeaders.size() > 0 && StringUtils.isNotBlank(categoryHeaders.get(0))) {
595                        TagList tagList = new TagList();
596                        for (String header : categoryHeaders) {
597                                parseTagValue(tagList, header);
598                        }
599                        if (resource instanceof IResource) {
600                                ResourceMetadataKeyEnum.TAG_LIST.put((IResource) resource, tagList);
601                        } else if (resource instanceof IAnyResource) {
602                                IBaseMetaType meta = ((IAnyResource) resource).getMeta();
603                                for (Tag next : tagList) {
604                                        meta.addTag().setSystem(next.getScheme()).setCode(next.getTerm()).setDisplay(next.getLabel());
605                                }
606                        }
607                }
608        }
609
610        public static String parseETagValue(String value) {
611                String eTagVersion;
612                value = value.trim();
613                if (value.length() > 1) {
614                        if (value.charAt(value.length() - 1) == '"') {
615                                if (value.charAt(0) == '"') {
616                                        eTagVersion = value.substring(1, value.length() - 1);
617                                } else if (value.length() > 3 && value.charAt(0) == 'W' && value.charAt(1) == '/' && value.charAt(2) == '"') {
618                                        eTagVersion = value.substring(3, value.length() - 1);
619                                } else {
620                                        eTagVersion = value;
621                                }
622                        } else {
623                                eTagVersion = value;
624                        }
625                } else {
626                        eTagVersion = value;
627                }
628                return eTagVersion;
629        }
630
631        /**
632         * This is a utility method intended provided to help the JPA module.
633         */
634        public static IQueryParameterAnd<?> parseQueryParams(FhirContext theContext, RuntimeSearchParam theParamDef, String theUnqualifiedParamName, List<QualifiedParamList> theParameters) {
635                RestSearchParameterTypeEnum paramType = theParamDef.getParamType();
636                return parseQueryParams(theContext, paramType, theUnqualifiedParamName, theParameters);
637        }
638
639
640        /**
641         * This is a utility method intended provided to help the JPA module.
642         */
643        public static IQueryParameterAnd<?> parseQueryParams(FhirContext theContext, RestSearchParameterTypeEnum paramType, String theUnqualifiedParamName, List<QualifiedParamList> theParameters) {
644                QueryParameterAndBinder binder = null;
645                switch (paramType) {
646                case COMPOSITE:
647                        throw new UnsupportedOperationException();
648                case DATE:
649                        binder = new QueryParameterAndBinder(DateAndListParam.class, Collections.<Class<? extends IQueryParameterType>> emptyList());
650                        break;
651                case NUMBER:
652                        binder = new QueryParameterAndBinder(NumberAndListParam.class, Collections.<Class<? extends IQueryParameterType>> emptyList());
653                        break;
654                case QUANTITY:
655                        binder = new QueryParameterAndBinder(QuantityAndListParam.class, Collections.<Class<? extends IQueryParameterType>> emptyList());
656                        break;
657                case REFERENCE:
658                        binder = new QueryParameterAndBinder(ReferenceAndListParam.class, Collections.<Class<? extends IQueryParameterType>> emptyList());
659                        break;
660                case STRING:
661                        binder = new QueryParameterAndBinder(StringAndListParam.class, Collections.<Class<? extends IQueryParameterType>> emptyList());
662                        break;
663                case TOKEN:
664                        binder = new QueryParameterAndBinder(TokenAndListParam.class, Collections.<Class<? extends IQueryParameterType>> emptyList());
665                        break;
666                case URI:
667                        binder = new QueryParameterAndBinder(UriAndListParam.class, Collections.<Class<? extends IQueryParameterType>> emptyList());
668                        break;
669                case HAS:
670                        binder = new QueryParameterAndBinder(HasAndListParam.class, Collections.<Class<? extends IQueryParameterType>> emptyList());
671                        break;
672                }
673
674                //FIXME null access
675                return binder.parse(theContext, theUnqualifiedParamName, theParameters);
676        }
677
678        public static void parseTagValue(TagList tagList, String nextTagComplete) {
679                StringBuilder next = new StringBuilder(nextTagComplete);
680                parseTagValue(tagList, nextTagComplete, next);
681        }
682
683        private static void parseTagValue(TagList theTagList, String theCompleteHeaderValue, StringBuilder theBuffer) {
684                int firstSemicolon = theBuffer.indexOf(";");
685                int deleteTo;
686                if (firstSemicolon == -1) {
687                        firstSemicolon = theBuffer.indexOf(",");
688                        if (firstSemicolon == -1) {
689                                firstSemicolon = theBuffer.length();
690                                deleteTo = theBuffer.length();
691                        } else {
692                                deleteTo = firstSemicolon;
693                        }
694                } else {
695                        deleteTo = firstSemicolon + 1;
696                }
697
698                String term = theBuffer.substring(0, firstSemicolon);
699                String scheme = null;
700                String label = null;
701                if (isBlank(term)) {
702                        return;
703                }
704
705                theBuffer.delete(0, deleteTo);
706                while (theBuffer.length() > 0 && theBuffer.charAt(0) == ' ') {
707                        theBuffer.deleteCharAt(0);
708                }
709
710                while (theBuffer.length() > 0) {
711                        boolean foundSomething = false;
712                        if (theBuffer.length() > SCHEME.length() && theBuffer.substring(0, SCHEME.length()).equals(SCHEME)) {
713                                int closeIdx = theBuffer.indexOf("\"", SCHEME.length());
714                                scheme = theBuffer.substring(SCHEME.length(), closeIdx);
715                                theBuffer.delete(0, closeIdx + 1);
716                                foundSomething = true;
717                        }
718                        if (theBuffer.length() > LABEL.length() && theBuffer.substring(0, LABEL.length()).equals(LABEL)) {
719                                int closeIdx = theBuffer.indexOf("\"", LABEL.length());
720                                label = theBuffer.substring(LABEL.length(), closeIdx);
721                                theBuffer.delete(0, closeIdx + 1);
722                                foundSomething = true;
723                        }
724                        // TODO: support enc2231-string as described in
725                        // http://tools.ietf.org/html/draft-johnston-http-category-header-02
726                        // TODO: support multiple tags in one header as described in
727                        // http://hl7.org/implement/standards/fhir/http.html#tags
728
729                        while (theBuffer.length() > 0 && (theBuffer.charAt(0) == ' ' || theBuffer.charAt(0) == ';')) {
730                                theBuffer.deleteCharAt(0);
731                        }
732
733                        if (!foundSomething) {
734                                break;
735                        }
736                }
737
738                if (theBuffer.length() > 0 && theBuffer.charAt(0) == ',') {
739                        theBuffer.deleteCharAt(0);
740                        while (theBuffer.length() > 0 && theBuffer.charAt(0) == ' ') {
741                                theBuffer.deleteCharAt(0);
742                        }
743                        theTagList.add(new Tag(scheme, term, label));
744                        parseTagValue(theTagList, theCompleteHeaderValue, theBuffer);
745                } else {
746                        theTagList.add(new Tag(scheme, term, label));
747                }
748
749                if (theBuffer.length() > 0) {
750                        ourLog.warn("Ignoring extra text at the end of " + Constants.HEADER_CATEGORY + " tag '" + theBuffer.toString() + "' - Complete tag value was: " + theCompleteHeaderValue);
751                }
752
753        }
754
755        public static MethodOutcome process2xxResponse(FhirContext theContext, int theResponseStatusCode, String theResponseMimeType, Reader theResponseReader, Map<String, List<String>> theHeaders) {
756                List<String> locationHeaders = new ArrayList<String>();
757                List<String> lh = theHeaders.get(Constants.HEADER_LOCATION_LC);
758                if (lh != null) {
759                        locationHeaders.addAll(lh);
760                }
761                List<String> clh = theHeaders.get(Constants.HEADER_CONTENT_LOCATION_LC);
762                if (clh != null) {
763                        locationHeaders.addAll(clh);
764                }
765
766                MethodOutcome retVal = new MethodOutcome();
767                if (locationHeaders != null && locationHeaders.size() > 0) {
768                        String locationHeader = locationHeaders.get(0);
769                        BaseOutcomeReturningMethodBinding.parseContentLocation(theContext, retVal, locationHeader);
770                }
771                if (theResponseStatusCode != Constants.STATUS_HTTP_204_NO_CONTENT) {
772                        EncodingEnum ct = EncodingEnum.forContentType(theResponseMimeType);
773                        if (ct != null) {
774                                PushbackReader reader = new PushbackReader(theResponseReader);
775
776                                try {
777                                        int firstByte = reader.read();
778                                        if (firstByte == -1) {
779                                                BaseOutcomeReturningMethodBinding.ourLog.debug("No content in response, not going to read");
780                                                reader = null;
781                                        } else {
782                                                reader.unread(firstByte);
783                                        }
784                                } catch (IOException e) {
785                                        BaseOutcomeReturningMethodBinding.ourLog.debug("No content in response, not going to read", e);
786                                        reader = null;
787                                }
788
789                                if (reader != null) {
790                                        IParser parser = ct.newParser(theContext);
791                                        IBaseResource outcome = parser.parseResource(reader);
792                                        if (outcome instanceof IBaseOperationOutcome) {
793                                                retVal.setOperationOutcome((IBaseOperationOutcome) outcome);
794                                        } else {
795                                                retVal.setResource(outcome);
796                                        }
797                                }
798
799                        } else {
800                                BaseOutcomeReturningMethodBinding.ourLog.debug("Ignoring response content of type: {}", theResponseMimeType);
801                        }
802                }
803                return retVal;
804        }
805
806        public static IQueryParameterOr<?> singleton(final IQueryParameterType theParam, final String theParamName) {
807                return new IQueryParameterOr<IQueryParameterType>() {
808
809                        @Override
810                        public List<IQueryParameterType> getValuesAsQueryTokens() {
811                                return Collections.singletonList(theParam);
812                        }
813
814                        @Override
815                        public void setValuesAsQueryTokens(FhirContext theContext, String theParamName, QualifiedParamList theParameters) {
816                                if (theParameters.isEmpty()) {
817                                        return;
818                                }
819                                if (theParameters.size() > 1) {
820                                        throw new IllegalArgumentException("Type " + theParam.getClass().getCanonicalName() + " does not support multiple values");
821                                }
822                                theParam.setValueAsQueryToken(theContext, theParamName, theParameters.getQualifier(), theParameters.get(0));
823                        }
824                };
825        }
826
827}