001/*
002 * #%L
003 * HAPI FHIR - Client Framework
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.client.method;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.model.api.IDatatype;
026import ca.uhn.fhir.model.api.IQueryParameterAnd;
027import ca.uhn.fhir.model.api.IQueryParameterOr;
028import ca.uhn.fhir.model.api.IQueryParameterType;
029import ca.uhn.fhir.rest.annotation.OperationParam;
030import ca.uhn.fhir.rest.api.ValidationModeEnum;
031import ca.uhn.fhir.rest.param.DateRangeParam;
032import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
033import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
034import ca.uhn.fhir.util.ParametersUtil;
035import org.apache.commons.lang3.Validate;
036import org.hl7.fhir.instance.model.api.IBase;
037import org.hl7.fhir.instance.model.api.IBaseParameters;
038import org.hl7.fhir.instance.model.api.IBaseResource;
039import org.hl7.fhir.instance.model.api.IPrimitiveType;
040
041import java.lang.reflect.Method;
042import java.lang.reflect.Modifier;
043import java.util.Collection;
044import java.util.List;
045import java.util.Map;
046
047public class OperationParameter implements IParameter {
048
049        @SuppressWarnings("unchecked")
050        private static final Class<? extends IQueryParameterType>[] COMPOSITE_TYPES = new Class[0];
051
052        static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE";
053
054        private final FhirContext myContext;
055        private IOperationParamConverter myConverter;
056
057        @SuppressWarnings("rawtypes")
058        private int myMax;
059
060        private int myMin;
061        private final String myName;
062        private Class<?> myParameterType;
063        private String myParamType;
064        private SearchParameter mySearchParameterBinding;
065
066        public OperationParameter(FhirContext theCtx, String theOperationName, OperationParam theOperationParam) {
067                this(theCtx, theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max());
068        }
069
070        OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin, int theMax) {
071                myName = theParameterName;
072                myMin = theMin;
073                myMax = theMax;
074                myContext = theCtx;
075        }
076
077        protected FhirContext getContext() {
078                return myContext;
079        }
080
081        public String getName() {
082                return myName;
083        }
084
085        public String getParamType() {
086                return myParamType;
087        }
088
089        public String getSearchParamType() {
090                if (mySearchParameterBinding != null) {
091                        return mySearchParameterBinding.getParamType().getCode();
092                }
093                return null;
094        }
095
096        @SuppressWarnings("unchecked")
097        @Override
098        public void initializeTypes(
099                        Method theMethod,
100                        Class<? extends Collection<?>> theOuterCollectionType,
101                        Class<? extends Collection<?>> theInnerCollectionType,
102                        Class<?> theParameterType) {
103                if (getContext().getVersion().getVersion().isRi()) {
104                        if (IDatatype.class.isAssignableFrom(theParameterType)) {
105                                throw new ConfigurationException(Msg.code(1408) + "Incorrect use of type "
106                                                + theParameterType.getSimpleName()
107                                                + " as parameter type for method when context is for version "
108                                                + getContext().getVersion().getVersion().name() + " in method: " + theMethod.toString());
109                        }
110                }
111
112                myParameterType = theParameterType;
113                if (theInnerCollectionType != null) {
114                        if (myMax == OperationParam.MAX_DEFAULT) {
115                                myMax = OperationParam.MAX_UNLIMITED;
116                        }
117                } else if (IQueryParameterAnd.class.isAssignableFrom(myParameterType)) {
118                        if (myMax == OperationParam.MAX_DEFAULT) {
119                                myMax = OperationParam.MAX_UNLIMITED;
120                        }
121                } else {
122                        if (myMax == OperationParam.MAX_DEFAULT) {
123                                myMax = 1;
124                        }
125                }
126
127                boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers());
128
129                // @formatter:off
130                boolean isSearchParam = IQueryParameterType.class.isAssignableFrom(myParameterType)
131                                || IQueryParameterOr.class.isAssignableFrom(myParameterType)
132                                || IQueryParameterAnd.class.isAssignableFrom(myParameterType);
133                // @formatter:off
134
135                /*
136                 * Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also
137                 * extend this interface. I'm not sure if they should in the end.. but they do, so we
138                 * exclude them.
139                 */
140                isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType);
141
142                /*
143                 * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We
144                 * should probably clean this up..
145                 */
146                if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) {
147                        if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) {
148                                myParamType = "Resource";
149                        } else if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
150                                myParamType = "date";
151                                myMax = 2;
152                        } else if (myParameterType.equals(ValidationModeEnum.class)) {
153                                myParamType = "code";
154                        } else if (IBase.class.isAssignableFrom(myParameterType) && typeIsConcrete) {
155                                myParamType = myContext
156                                                .getElementDefinition((Class<? extends IBase>) myParameterType)
157                                                .getName();
158                        } else if (isSearchParam) {
159                                myParamType = "string";
160                                mySearchParameterBinding = new SearchParameter(myName, myMin > 0);
161                                mySearchParameterBinding.setCompositeTypes(COMPOSITE_TYPES);
162                                mySearchParameterBinding.setType(
163                                                myContext, theParameterType, theInnerCollectionType, theOuterCollectionType);
164                                myConverter = new OperationParamConverter();
165                        } else {
166                                throw new ConfigurationException(
167                                                Msg.code(1409) + "Invalid type for @OperationParam: " + myParameterType.getName());
168                        }
169                }
170        }
171
172        public OperationParameter setConverter(IOperationParamConverter theConverter) {
173                myConverter = theConverter;
174                return this;
175        }
176
177        @Override
178        public void translateClientArgumentIntoQueryArgument(
179                        FhirContext theContext,
180                        Object theSourceClientArgument,
181                        Map<String, List<String>> theTargetQueryArguments,
182                        IBaseResource theTargetResource)
183                        throws InternalErrorException {
184                assert theTargetResource != null;
185                Object sourceClientArgument = theSourceClientArgument;
186                if (sourceClientArgument == null) {
187                        return;
188                }
189
190                if (myConverter != null) {
191                        sourceClientArgument = myConverter.outgoingClient(sourceClientArgument);
192                }
193
194                ParametersUtil.addParameterToParameters(
195                                theContext, (IBaseParameters) theTargetResource, myName, sourceClientArgument);
196        }
197
198        public static void throwInvalidMode(String paramValues) {
199                throw new InvalidRequestException(Msg.code(1410) + "Invalid mode value: \"" + paramValues + "\"");
200        }
201
202        interface IOperationParamConverter {
203
204                Object outgoingClient(Object theObject);
205        }
206
207        class OperationParamConverter implements IOperationParamConverter {
208
209                public OperationParamConverter() {
210                        Validate.isTrue(mySearchParameterBinding != null);
211                }
212
213                @Override
214                public Object outgoingClient(Object theObject) {
215                        IQueryParameterType obj = (IQueryParameterType) theObject;
216                        IPrimitiveType<?> retVal =
217                                        (IPrimitiveType<?>) myContext.getElementDefinition("string").newInstance();
218                        retVal.setValueAsString(obj.getValueAsQueryToken(myContext));
219                        return retVal;
220                }
221        }
222}