001package ca.uhn.fhir.rest.method;
002
003import static org.apache.commons.lang3.StringUtils.isNotBlank;
004
005/*
006 * #%L
007 * HAPI FHIR - Core Library
008 * %%
009 * Copyright (C) 2014 - 2017 University Health Network
010 * %%
011 * Licensed under the Apache License, Version 2.0 (the "License");
012 * you may not use this file except in compliance with the License.
013 * You may obtain a copy of the License at
014 * 
015 *      http://www.apache.org/licenses/LICENSE-2.0
016 * 
017 * Unless required by applicable law or agreed to in writing, software
018 * distributed under the License is distributed on an "AS IS" BASIS,
019 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
020 * See the License for the specific language governing permissions and
021 * limitations under the License.
022 * #L%
023 */
024
025import java.lang.reflect.Method;
026import java.lang.reflect.Modifier;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.List;
032import java.util.Map;
033
034import org.apache.commons.lang3.Validate;
035import org.hl7.fhir.instance.model.api.IBase;
036import org.hl7.fhir.instance.model.api.IBaseDatatype;
037import org.hl7.fhir.instance.model.api.IBaseResource;
038import org.hl7.fhir.instance.model.api.IPrimitiveType;
039
040import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
041import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IAccessor;
042import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
043import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
044import ca.uhn.fhir.context.ConfigurationException;
045import ca.uhn.fhir.context.FhirContext;
046import ca.uhn.fhir.context.IRuntimeDatatypeDefinition;
047import ca.uhn.fhir.context.RuntimeChildPrimitiveDatatypeDefinition;
048import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
049import ca.uhn.fhir.context.RuntimeResourceDefinition;
050import ca.uhn.fhir.i18n.HapiLocalizer;
051import ca.uhn.fhir.model.api.IDatatype;
052import ca.uhn.fhir.model.api.IQueryParameterAnd;
053import ca.uhn.fhir.model.api.IQueryParameterOr;
054import ca.uhn.fhir.model.api.IQueryParameterType;
055import ca.uhn.fhir.rest.annotation.OperationParam;
056import ca.uhn.fhir.rest.api.RequestTypeEnum;
057import ca.uhn.fhir.rest.api.ValidationModeEnum;
058import ca.uhn.fhir.rest.param.BaseAndListParam;
059import ca.uhn.fhir.rest.param.CollectionBinder;
060import ca.uhn.fhir.rest.param.DateRangeParam;
061import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
062import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
063import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
064import ca.uhn.fhir.util.FhirTerser;
065import ca.uhn.fhir.util.ParametersUtil;
066import ca.uhn.fhir.util.ReflectionUtil;
067
068public class OperationParameter implements IParameter {
069
070        @SuppressWarnings("unchecked")
071        private static final Class<? extends IQueryParameterType>[] COMPOSITE_TYPES = new Class[0];
072
073        static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE";
074
075        private boolean myAllowGet;
076
077        private final FhirContext myContext;
078        private IOperationParamConverter myConverter;
079        @SuppressWarnings("rawtypes")
080        private Class<? extends Collection> myInnerCollectionType;
081        private int myMax;
082        private int myMin;
083        private final String myName;
084        private final String myOperationName;
085        private Class<?> myParameterType;
086        private String myParamType;
087        private SearchParameter mySearchParameterBinding;
088
089        public OperationParameter(FhirContext theCtx, String theOperationName, OperationParam theOperationParam) {
090                this(theCtx, theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max());
091        }
092
093        OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin, int theMax) {
094                myOperationName = theOperationName;
095                myName = theParameterName;
096                myMin = theMin;
097                myMax = theMax;
098                myContext = theCtx;
099        }
100
101        @SuppressWarnings({ "rawtypes", "unchecked" })
102        private void addValueToList(List<Object> matchingParamValues, Object values) {
103                if (values != null) {
104                        if (BaseAndListParam.class.isAssignableFrom(myParameterType) && matchingParamValues.size() > 0) {
105                                BaseAndListParam existing = (BaseAndListParam<?>) matchingParamValues.get(0);
106                                BaseAndListParam<?> newAndList = (BaseAndListParam<?>) values;
107                                for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) {
108                                        existing.addAnd(nextAnd);
109                                }
110                        } else {
111                                matchingParamValues.add(values);
112                        }
113                }
114        }
115
116        protected FhirContext getContext() {
117                return myContext;
118        }
119
120        public int getMax() {
121                return myMax;
122        }
123
124        public int getMin() {
125                return myMin;
126        }
127
128        public String getName() {
129                return myName;
130        }
131
132        public String getParamType() {
133                return myParamType;
134        }
135
136        public String getSearchParamType() {
137                if (mySearchParameterBinding != null) {
138                        return mySearchParameterBinding.getParamType().getCode();
139                }
140                return null;
141        }
142
143        @SuppressWarnings("unchecked")
144        @Override
145        public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType, Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) {
146                if (getContext().getVersion().getVersion().isRi()) {
147                        if (IDatatype.class.isAssignableFrom(theParameterType)) {
148                                throw new ConfigurationException("Incorrect use of type " + theParameterType.getSimpleName() + " as parameter type for method when context is for version " + getContext().getVersion().getVersion().name() + " in method: " + theMethod.toString());
149                        }
150                }
151
152                myParameterType = theParameterType;
153                if (theInnerCollectionType != null) {
154                        myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName);
155                        if (myMax == OperationParam.MAX_DEFAULT) {
156                                myMax = OperationParam.MAX_UNLIMITED;
157                        }
158                } else if (IQueryParameterAnd.class.isAssignableFrom(myParameterType)) {
159                        if (myMax == OperationParam.MAX_DEFAULT) {
160                                myMax = OperationParam.MAX_UNLIMITED;
161                        }
162                } else {
163                        if (myMax == OperationParam.MAX_DEFAULT) {
164                                myMax = 1;
165                        }
166                }
167
168                boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers());
169
170                //@formatter:off
171                boolean isSearchParam = 
172                        IQueryParameterType.class.isAssignableFrom(myParameterType) || 
173                        IQueryParameterOr.class.isAssignableFrom(myParameterType) ||
174                        IQueryParameterAnd.class.isAssignableFrom(myParameterType); 
175                //@formatter:off
176
177                /*
178                 * Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also
179                 * extend this interface. I'm not sure if they should in the end.. but they do, so we
180                 * exclude them.
181                 */
182                isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType);
183
184                myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType) || isSearchParam || ValidationModeEnum.class.equals(myParameterType);
185
186                /*
187                 * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We
188                 * should probably clean this up..
189                 */
190                if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) {
191                        if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) {
192                                myParamType = "Resource";
193                        } else if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
194                                myParamType = "date";
195                                myMax = 2;
196                                myAllowGet = true;
197                        } else if (myParameterType.equals(ValidationModeEnum.class)) {
198                                myParamType = "code";
199                        } else if (IBase.class.isAssignableFrom(myParameterType) && typeIsConcrete) {
200                                myParamType = myContext.getElementDefinition((Class<? extends IBase>) myParameterType).getName();
201                        } else if (isSearchParam) {
202                                myParamType = "string";
203                                mySearchParameterBinding = new SearchParameter(myName, myMin > 0);
204                                mySearchParameterBinding.setCompositeTypes(COMPOSITE_TYPES);
205                                mySearchParameterBinding.setType(myContext, theParameterType, theInnerCollectionType, theOuterCollectionType);
206                                myConverter = new OperationParamConverter();
207                        } else {
208                                throw new ConfigurationException("Invalid type for @OperationParam: " + myParameterType.getName());
209                        }
210
211                }
212
213        }
214
215        public OperationParameter setConverter(IOperationParamConverter theConverter) {
216                myConverter = theConverter;
217                return this;
218        }
219
220        private void throwWrongParamType(Object nextValue) {
221                throw new InvalidRequestException("Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName());
222        }
223
224        @Override
225        public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
226                assert theTargetResource != null;
227                Object sourceClientArgument = theSourceClientArgument;
228                if (sourceClientArgument == null) {
229                        return;
230                }
231
232                if (myConverter != null) {
233                        sourceClientArgument = myConverter.outgoingClient(sourceClientArgument);
234                }
235
236                ParametersUtil.addParameterToParameters(theContext, theTargetResource, sourceClientArgument, myName);
237        }
238
239        @SuppressWarnings("unchecked")
240        @Override
241        public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException {
242                List<Object> matchingParamValues = new ArrayList<Object>();
243
244                if (theRequest.getRequestType() == RequestTypeEnum.GET) {
245                        translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues);
246                } else {
247                        translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues);
248                }
249
250                if (matchingParamValues.isEmpty()) {
251                        return null;
252                }
253
254                if (myInnerCollectionType == null) {
255                        return matchingParamValues.get(0);
256                }
257
258                Collection<Object> retVal = ReflectionUtil.newInstance(myInnerCollectionType);
259                retVal.addAll(matchingParamValues);
260                return retVal;
261        }
262
263        private void translateQueryParametersIntoServerArgumentForGet(RequestDetails theRequest, List<Object> matchingParamValues) {
264                if (mySearchParameterBinding != null) {
265
266                        List<QualifiedParamList> params = new ArrayList<QualifiedParamList>();
267                        String nameWithQualifierColon = myName + ":";
268
269                        for (String nextParamName : theRequest.getParameters().keySet()) {
270                                String qualifier;
271                                if (nextParamName.equals(myName)) {
272                                        qualifier = null;
273                                } else if (nextParamName.startsWith(nameWithQualifierColon)) {
274                                        qualifier = nextParamName.substring(nextParamName.indexOf(':'));
275                                } else {
276                                        // This is some other parameter, not the one bound by this instance
277                                        continue;
278                                }
279                                String[] values = theRequest.getParameters().get(nextParamName);
280                                if (values != null) {
281                                        for (String nextValue : values) {
282                                                params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue));
283                                        }
284                                }
285                        }
286                        if (!params.isEmpty()) {
287                                for (QualifiedParamList next : params) {
288                                        Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next));
289                                        addValueToList(matchingParamValues, values);
290                                }
291                                
292                        }
293
294                } else {
295                        String[] paramValues = theRequest.getParameters().get(myName);
296                        if (paramValues != null && paramValues.length > 0) {
297                                if (myAllowGet) {
298
299                                        if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
300                                                List<QualifiedParamList> parameters = new ArrayList<QualifiedParamList>();
301                                                parameters.add(QualifiedParamList.singleton(paramValues[0]));
302                                                if (paramValues.length > 1) {
303                                                        parameters.add(QualifiedParamList.singleton(paramValues[1]));
304                                                }
305                                                DateRangeParam dateRangeParam = new DateRangeParam();
306                                                FhirContext ctx = theRequest.getServer().getFhirContext();
307                                                dateRangeParam.setValuesAsQueryTokens(ctx, myName, parameters);
308                                                matchingParamValues.add(dateRangeParam);
309                                        } else if (String.class.isAssignableFrom(myParameterType)) {
310
311                                                for (String next : paramValues) {
312                                                        matchingParamValues.add(next);
313                                                }
314                                        } else if (ValidationModeEnum.class.equals(myParameterType)) {
315                                                
316                                                if (isNotBlank(paramValues[0])) {
317                                                        ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]);
318                                                        if (validationMode != null) {
319                                                                matchingParamValues.add(validationMode);
320                                                        } else {
321                                                                throwInvalidMode(paramValues[0]);
322                                                        }
323                                                }
324                                                
325                                        } else {
326                                                for (String nextValue : paramValues) {
327                                                        FhirContext ctx = theRequest.getServer().getFhirContext();
328                                                        RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) ctx.getElementDefinition(myParameterType.asSubclass(IBase.class));
329                                                        IPrimitiveType<?> instance = def.newInstance();
330                                                        instance.setValueAsString(nextValue);
331                                                        matchingParamValues.add(instance);
332                                                }
333                                        }
334                                } else {
335                                        HapiLocalizer localizer = theRequest.getServer().getFhirContext().getLocalizer();
336                                        String msg = localizer.getMessage(OperationParameter.class, "urlParamNotPrimitive", myOperationName, myName);
337                                        throw new MethodNotAllowedException(msg, RequestTypeEnum.POST);
338                                }
339                        }
340                }
341        }
342
343        private void translateQueryParametersIntoServerArgumentForPost(RequestDetails theRequest, List<Object> matchingParamValues) {
344                IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY);
345                RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents);
346                if (def.getName().equals("Parameters")) {
347
348                        BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
349                        BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
350
351                        RuntimeChildPrimitiveDatatypeDefinition nameChild = (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name");
352                        BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]");
353                        BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource");
354
355                        IAccessor paramChildAccessor = paramChild.getAccessor();
356                        List<IBase> values = paramChildAccessor.getValues(requestContents);
357                        for (IBase nextParameter : values) {
358                                List<IBase> nextNames = nameChild.getAccessor().getValues(nextParameter);
359                                if (nextNames != null && nextNames.size() > 0) {
360                                        IPrimitiveType<?> nextName = (IPrimitiveType<?>) nextNames.get(0);
361                                        if (myName.equals(nextName.getValueAsString())) {
362
363                                                if (myParameterType.isAssignableFrom(nextParameter.getClass())) {
364                                                        matchingParamValues.add(nextParameter);
365                                                } else {
366                                                        List<IBase> paramValues = valueChild.getAccessor().getValues(nextParameter);
367                                                        List<IBase> paramResources = resourceChild.getAccessor().getValues(nextParameter);
368                                                        if (paramValues != null && paramValues.size() > 0) {
369                                                                tryToAddValues(paramValues, matchingParamValues);
370                                                        } else if (paramResources != null && paramResources.size() > 0) {
371                                                                tryToAddValues(paramResources, matchingParamValues);
372                                                        }
373                                                }
374
375                                        }
376                                }
377                        }
378
379                } else {
380
381                        if (myParameterType.isAssignableFrom(requestContents.getClass())) {
382                                tryToAddValues(Arrays.asList((IBase) requestContents), matchingParamValues);
383                        }
384
385                }
386        }
387
388        @SuppressWarnings("unchecked")
389        private void tryToAddValues(List<IBase> theParamValues, List<Object> theMatchingParamValues) {
390                for (Object nextValue : theParamValues) {
391                        if (nextValue == null) {
392                                continue;
393                        }
394                        if (myConverter != null) {
395                                nextValue = myConverter.incomingServer(nextValue);
396                        }
397                        if (!myParameterType.isAssignableFrom(nextValue.getClass())) {
398                                Class<? extends IBaseDatatype> sourceType = (Class<? extends IBaseDatatype>) nextValue.getClass();
399                                Class<? extends IBaseDatatype> targetType = (Class<? extends IBaseDatatype>) myParameterType;
400                                BaseRuntimeElementDefinition<?> sourceTypeDef = myContext.getElementDefinition(sourceType);
401                                BaseRuntimeElementDefinition<?> targetTypeDef = myContext.getElementDefinition(targetType);
402                                if (targetTypeDef instanceof IRuntimeDatatypeDefinition && sourceTypeDef instanceof IRuntimeDatatypeDefinition) {
403                                        IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef;
404                                        if (targetTypeDtDef.isProfileOf(sourceType)) {
405                                                FhirTerser terser = myContext.newTerser();
406                                                IBase newTarget = targetTypeDef.newInstance();
407                                                terser.cloneInto((IBase) nextValue, newTarget, true);
408                                                theMatchingParamValues.add(newTarget);
409                                                continue;
410                                        }
411                                }
412                                throwWrongParamType(nextValue);
413                        }
414                        
415                        addValueToList(theMatchingParamValues, nextValue);
416                }
417        }
418
419        public static void throwInvalidMode(String paramValues) {
420                throw new InvalidRequestException("Invalid mode value: \"" + paramValues + "\"");
421        }
422
423        interface IOperationParamConverter {
424
425                Object incomingServer(Object theObject);
426
427                Object outgoingClient(Object theObject);
428
429        }
430
431        class OperationParamConverter implements IOperationParamConverter {
432
433                public OperationParamConverter() {
434                        Validate.isTrue(mySearchParameterBinding != null);
435                }
436
437                @Override
438                public Object incomingServer(Object theObject) {
439                        IPrimitiveType<?> obj = (IPrimitiveType<?>) theObject;
440                        List<QualifiedParamList> paramList = Collections.singletonList(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString()));
441                        return mySearchParameterBinding.parse(myContext, paramList);
442                }
443
444                @Override
445                public Object outgoingClient(Object theObject) {
446                        IQueryParameterType obj = (IQueryParameterType) theObject;
447                        IPrimitiveType<?> retVal = (IPrimitiveType<?>) myContext.getElementDefinition("string").newInstance();
448                        retVal.setValueAsString(obj.getValueAsQueryToken(myContext));
449                        return retVal;
450                }
451
452        }
453
454
455}