001package ca.uhn.fhir.rest.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import java.lang.reflect.Method;
024import java.lang.reflect.Modifier;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collection;
028import java.util.List;
029import java.util.Map;
030
031import org.hl7.fhir.instance.model.api.IBase;
032import org.hl7.fhir.instance.model.api.IBaseDatatype;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034import org.hl7.fhir.instance.model.api.IPrimitiveType;
035
036import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
037import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IAccessor;
038import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
039import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
040import ca.uhn.fhir.context.ConfigurationException;
041import ca.uhn.fhir.context.FhirContext;
042import ca.uhn.fhir.context.IRuntimeDatatypeDefinition;
043import ca.uhn.fhir.context.RuntimeChildPrimitiveDatatypeDefinition;
044import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
045import ca.uhn.fhir.context.RuntimeResourceDefinition;
046import ca.uhn.fhir.i18n.HapiLocalizer;
047import ca.uhn.fhir.model.api.IDatatype;
048import ca.uhn.fhir.rest.annotation.OperationParam;
049import ca.uhn.fhir.rest.api.RequestTypeEnum;
050import ca.uhn.fhir.rest.api.ValidationModeEnum;
051import ca.uhn.fhir.rest.param.CollectionBinder;
052import ca.uhn.fhir.rest.param.DateRangeParam;
053import ca.uhn.fhir.rest.param.ResourceParameter;
054import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
055import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
056import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
057import ca.uhn.fhir.util.FhirTerser;
058import ca.uhn.fhir.util.ParametersUtil;
059
060public class OperationParameter implements IParameter {
061
062        private IConverter myConverter;
063        @SuppressWarnings("rawtypes")
064        private Class<? extends Collection> myInnerCollectionType;
065        private int myMax;
066        private int myMin;
067        private final String myName;
068        private final String myOperationName;
069        private Class<?> myParameterType;
070        private String myParamType;
071        private final FhirContext myContext;
072        private boolean myAllowGet;
073
074        public OperationParameter(FhirContext theCtx, String theOperationName, OperationParam theOperationParam) {
075                this(theCtx, theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max());
076        }
077
078        OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin, int theMax) {
079                myOperationName = theOperationName;
080                myName = theParameterName;
081                myMin = theMin;
082                myMax = theMax;
083                myContext = theCtx;
084        }
085
086        protected FhirContext getContext() {
087                return myContext;
088        }
089
090        public int getMax() {
091                return myMax;
092        }
093
094        public int getMin() {
095                return myMin;
096        }
097
098        public String getName() {
099                return myName;
100        }
101
102        public String getParamType() {
103                return myParamType;
104        }
105
106        @SuppressWarnings("unchecked")
107        @Override
108        public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType, Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) {
109                if (getContext().getVersion().getVersion().isRi()) {
110                        if (IDatatype.class.isAssignableFrom(theParameterType)) {
111                                throw new ConfigurationException("Incorrect use of type " + theParameterType.getSimpleName() + " as parameter type for method when context is for version "
112                                                + getContext().getVersion().getVersion().name() + " in method: " + theMethod.toString());
113                        }
114                }
115
116                myParameterType = theParameterType;
117                if (theInnerCollectionType != null) {
118                        myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName);
119                } else {
120                        myMax = 1;
121                }
122
123                myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType);
124
125                /*
126                 * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We should probably clean this up..
127                 */
128                if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) {
129                        if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) {
130                                myParamType = "Resource";
131                        } else if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
132                                myParamType = "date";
133                                myMax = 2;
134                                myAllowGet = true;
135                        } else if (!IBase.class.isAssignableFrom(myParameterType) || myParameterType.isInterface() || Modifier.isAbstract(myParameterType.getModifiers())) {
136                                throw new ConfigurationException("Invalid type for @OperationParam: " + myParameterType.getName());
137                        } else if (myParameterType.equals(ValidationModeEnum.class)) {
138                                myParamType = "code";
139                        } else {
140                                myParamType = myContext.getElementDefinition((Class<? extends IBase>) myParameterType).getName();
141                        }
142                }
143
144        }
145
146        public OperationParameter setConverter(IConverter theConverter) {
147                myConverter = theConverter;
148                return this;
149        }
150
151        @Override
152        public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource)
153                        throws InternalErrorException {
154                assert theTargetResource != null;
155                Object sourceClientArgument = theSourceClientArgument;
156                if (sourceClientArgument == null) {
157                        return;
158                }
159
160                if (myConverter != null) {
161                        sourceClientArgument = myConverter.outgoingClient(sourceClientArgument);
162                }
163
164                ParametersUtil.addParameterToParameters(theContext, theTargetResource, sourceClientArgument, myName);
165        }
166
167        @SuppressWarnings("unchecked")
168        @Override
169        public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException {
170                List<Object> matchingParamValues = new ArrayList<Object>();
171
172                if (theRequest.getRequestType() == RequestTypeEnum.GET) {
173                        String[] paramValues = theRequest.getParameters().get(myName);
174                        if (paramValues != null && paramValues.length > 0) {
175                                if (myAllowGet) {
176
177                                        if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
178                                                List<QualifiedParamList> parameters = new ArrayList<QualifiedParamList>();
179                                                parameters.add(QualifiedParamList.singleton(paramValues[0]));
180                                                if (paramValues.length > 1) {
181                                                        parameters.add(QualifiedParamList.singleton(paramValues[1]));
182                                                }
183                                                DateRangeParam dateRangeParam = new DateRangeParam();
184                                                dateRangeParam.setValuesAsQueryTokens(parameters);
185                                                matchingParamValues.add(dateRangeParam);
186                                        } else if (String.class.isAssignableFrom(myParameterType)) {
187
188                                                for (String next : paramValues) {
189                                                        matchingParamValues.add(next);
190                                                }
191
192                                        } else {
193                                                for (String nextValue : paramValues) {
194                                                        FhirContext ctx = theRequest.getServer().getFhirContext();
195                                                        RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) ctx.getElementDefinition((Class<? extends IBase>) myParameterType);
196                                                        IPrimitiveType<?> instance = def.newInstance();
197                                                        instance.setValueAsString(nextValue);
198                                                        matchingParamValues.add(instance);
199                                                }
200                                        }
201                                } else {
202                                        HapiLocalizer localizer = theRequest.getServer().getFhirContext().getLocalizer();
203                                        String msg = localizer.getMessage(OperationParameter.class, "urlParamNotPrimitive", myOperationName, myName);
204                                        throw new MethodNotAllowedException(msg, RequestTypeEnum.POST);
205                                }
206                        }
207                } else {
208
209                        FhirContext ctx = theRequest.getServer().getFhirContext();
210
211                        if (theRequest.getRequestType() == RequestTypeEnum.GET) {
212                                return null;
213                        }
214
215                        // Class<? extends IBaseResource> wantedResourceType = theMethodBinding.getContext().getResourceDefinition("Parameters").getImplementingClass();
216                        Class<IBaseResource> wantedResourceType = null;
217                        IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, theMethodBinding, wantedResourceType);
218
219                        RuntimeResourceDefinition def = ctx.getResourceDefinition(requestContents);
220                        if (def.getName().equals("Parameters")) {
221
222                                BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
223                                BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter");
224
225                                RuntimeChildPrimitiveDatatypeDefinition nameChild = (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name");
226                                BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]");
227                                BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource");
228
229                                IAccessor paramChildAccessor = paramChild.getAccessor();
230                                List<IBase> values = paramChildAccessor.getValues(requestContents);
231                                for (IBase nextParameter : values) {
232                                        List<IBase> nextNames = nameChild.getAccessor().getValues(nextParameter);
233                                        if (nextNames != null && nextNames.size() > 0) {
234                                                IPrimitiveType<?> nextName = (IPrimitiveType<?>) nextNames.get(0);
235                                                if (myName.equals(nextName.getValueAsString())) {
236
237                                                        if (myParameterType.isAssignableFrom(nextParameter.getClass())) {
238                                                                matchingParamValues.add(nextParameter);
239                                                        } else {
240                                                                List<IBase> paramValues = valueChild.getAccessor().getValues(nextParameter);
241                                                                List<IBase> paramResources = resourceChild.getAccessor().getValues(nextParameter);
242                                                                if (paramValues != null && paramValues.size() > 0) {
243                                                                        tryToAddValues(paramValues, matchingParamValues);
244                                                                } else if (paramResources != null && paramResources.size() > 0) {
245                                                                        tryToAddValues(paramResources, matchingParamValues);
246                                                                }
247                                                        }
248
249                                                }
250                                        }
251                                }
252
253                        } else {
254                                
255                                if (myParameterType.isAssignableFrom(requestContents.getClass())) {
256                                        tryToAddValues(Arrays.asList((IBase) requestContents), matchingParamValues);
257                                }
258
259                        }
260                }
261
262                if (matchingParamValues.isEmpty()) {
263                        return null;
264                }
265
266                if (myInnerCollectionType == null) {
267                        return matchingParamValues.get(0);
268                }
269
270                try {
271                        @SuppressWarnings("rawtypes")
272                        Collection retVal = myInnerCollectionType.newInstance();
273                        retVal.addAll(matchingParamValues);
274                        return retVal;
275                } catch (InstantiationException e) {
276                        throw new InternalErrorException("Failed to instantiate " + myInnerCollectionType, e);
277                } catch (IllegalAccessException e) {
278                        throw new InternalErrorException("Failed to instantiate " + myInnerCollectionType, e);
279                }
280        }
281
282        @SuppressWarnings("unchecked")
283        private void tryToAddValues(List<IBase> theParamValues, List<Object> theMatchingParamValues) {
284                for (Object nextValue : theParamValues) {
285                        if (nextValue == null) {
286                                continue;
287                        }
288                        if (myConverter != null) {
289                                nextValue = myConverter.incomingServer(nextValue);
290                        }
291                        if (!myParameterType.isAssignableFrom(nextValue.getClass())) {
292                                Class<? extends IBaseDatatype> sourceType = (Class<? extends IBaseDatatype>) nextValue.getClass();
293                                Class<? extends IBaseDatatype> targetType = (Class<? extends IBaseDatatype>) myParameterType;
294                                BaseRuntimeElementDefinition<?> sourceTypeDef = myContext.getElementDefinition(sourceType);
295                                BaseRuntimeElementDefinition<?> targetTypeDef = myContext.getElementDefinition(targetType);
296                                if (targetTypeDef instanceof IRuntimeDatatypeDefinition && sourceTypeDef instanceof IRuntimeDatatypeDefinition) {
297                                        IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef;
298                                        if (targetTypeDtDef.isProfileOf(sourceType)) {
299                                                FhirTerser terser = myContext.newTerser();
300                                                IBase newTarget = targetTypeDef.newInstance();
301                                                terser.cloneInto((IBase) nextValue, newTarget, true);
302                                                theMatchingParamValues.add(newTarget);
303                                                continue;
304                                        }
305                                }
306                                throwWrongParamType(nextValue);
307                        }
308                        theMatchingParamValues.add(nextValue);
309                }
310        }
311
312        private void throwWrongParamType(Object nextValue) {
313                throw new InvalidRequestException("Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName());
314        }
315
316        public interface IConverter {
317
318                Object incomingServer(Object theObject);
319
320                Object outgoingClient(Object theObject);
321
322        }
323
324}