ca.uhn.fhir.rest.method.OperationParameter.java Source code

Java tutorial

Introduction

Here is the source code for ca.uhn.fhir.rest.method.OperationParameter.java

Source

package ca.uhn.fhir.rest.method;

import static org.apache.commons.lang3.StringUtils.isNotBlank;

/*
 * #%L
 * HAPI FHIR - Core Library
 * %%
 * Copyright (C) 2014 - 2016 University Health Network
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;

import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IAccessor;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.IRuntimeDatatypeDefinition;
import ca.uhn.fhir.context.RuntimeChildPrimitiveDatatypeDefinition;
import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.model.api.IDatatype;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.param.BaseAndListParam;
import ca.uhn.fhir.rest.param.CollectionBinder;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.ReflectionUtil;

public class OperationParameter implements IParameter {

    @SuppressWarnings("unchecked")
    private static final Class<? extends IQueryParameterType>[] COMPOSITE_TYPES = new Class[0];

    static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE";

    private boolean myAllowGet;

    private final FhirContext myContext;
    private IOperationParamConverter myConverter;
    @SuppressWarnings("rawtypes")
    private Class<? extends Collection> myInnerCollectionType;
    private int myMax;
    private int myMin;
    private final String myName;
    private final String myOperationName;
    private Class<?> myParameterType;
    private String myParamType;
    private SearchParameter mySearchParameterBinding;

    public OperationParameter(FhirContext theCtx, String theOperationName, OperationParam theOperationParam) {
        this(theCtx, theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max());
    }

    OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin,
            int theMax) {
        myOperationName = theOperationName;
        myName = theParameterName;
        myMin = theMin;
        myMax = theMax;
        myContext = theCtx;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void addValueToList(List<Object> matchingParamValues, Object values) {
        if (values != null) {
            if (BaseAndListParam.class.isAssignableFrom(myParameterType) && matchingParamValues.size() > 0) {
                BaseAndListParam existing = (BaseAndListParam<?>) matchingParamValues.get(0);
                BaseAndListParam<?> newAndList = (BaseAndListParam<?>) values;
                for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) {
                    existing.addAnd(nextAnd);
                }
            } else {
                matchingParamValues.add(values);
            }
        }
    }

    protected FhirContext getContext() {
        return myContext;
    }

    public int getMax() {
        return myMax;
    }

    public int getMin() {
        return myMin;
    }

    public String getName() {
        return myName;
    }

    public String getParamType() {
        return myParamType;
    }

    public String getSearchParamType() {
        if (mySearchParameterBinding != null) {
            return mySearchParameterBinding.getParamType().getCode();
        } else {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType,
            Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) {
        if (getContext().getVersion().getVersion().isRi()) {
            if (IDatatype.class.isAssignableFrom(theParameterType)) {
                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());
            }
        }

        myParameterType = theParameterType;
        if (theInnerCollectionType != null) {
            myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName);
            if (myMax == OperationParam.MAX_DEFAULT) {
                myMax = OperationParam.MAX_UNLIMITED;
            }
        } else if (IQueryParameterAnd.class.isAssignableFrom(myParameterType)) {
            if (myMax == OperationParam.MAX_DEFAULT) {
                myMax = OperationParam.MAX_UNLIMITED;
            }
        } else {
            if (myMax == OperationParam.MAX_DEFAULT) {
                myMax = 1;
            }
        }

        boolean typeIsConcrete = !myParameterType.isInterface()
                && !Modifier.isAbstract(myParameterType.getModifiers());

        //@formatter:off
        boolean isSearchParam = IQueryParameterType.class.isAssignableFrom(myParameterType)
                || IQueryParameterOr.class.isAssignableFrom(myParameterType)
                || IQueryParameterAnd.class.isAssignableFrom(myParameterType);
        //@formatter:off

        /*
         * Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also
         * extend this interface. I'm not sure if they should in the end.. but they do, so we
         * exclude them.
         */
        isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType);

        myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType)
                || isSearchParam || ValidationModeEnum.class.equals(myParameterType);

        /*
         * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We
         * should probably clean this up..
         */
        if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) {
            if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) {
                myParamType = "Resource";
            } else if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
                myParamType = "date";
                myMax = 2;
                myAllowGet = true;
            } else if (myParameterType.equals(ValidationModeEnum.class)) {
                myParamType = "code";
            } else if (IBase.class.isAssignableFrom(myParameterType) && typeIsConcrete) {
                myParamType = myContext.getElementDefinition((Class<? extends IBase>) myParameterType).getName();
            } else if (isSearchParam) {
                myParamType = "string";
                mySearchParameterBinding = new SearchParameter(myName, myMin > 0);
                mySearchParameterBinding.setCompositeTypes(COMPOSITE_TYPES);
                mySearchParameterBinding.setType(myContext, theParameterType, theInnerCollectionType,
                        theOuterCollectionType);
                myConverter = new OperationParamConverter();
            } else {
                throw new ConfigurationException("Invalid type for @OperationParam: " + myParameterType.getName());
            }

        }

    }

    public OperationParameter setConverter(IOperationParamConverter theConverter) {
        myConverter = theConverter;
        return this;
    }

    private void throwWrongParamType(Object nextValue) {
        throw new InvalidRequestException(
                "Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName()
                        + " but method expects type " + myParameterType.getSimpleName());
    }

    @Override
    public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument,
            Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource)
            throws InternalErrorException {
        assert theTargetResource != null;
        Object sourceClientArgument = theSourceClientArgument;
        if (sourceClientArgument == null) {
            return;
        }

        if (myConverter != null) {
            sourceClientArgument = myConverter.outgoingClient(sourceClientArgument);
        }

        ParametersUtil.addParameterToParameters(theContext, theTargetResource, sourceClientArgument, myName);
    }

    @SuppressWarnings("unchecked")
    @Override
    public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest,
            BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException {
        List<Object> matchingParamValues = new ArrayList<Object>();

        if (theRequest.getRequestType() == RequestTypeEnum.GET) {
            translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues);
        } else {
            translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues);
        }

        if (matchingParamValues.isEmpty()) {
            return null;
        }

        if (myInnerCollectionType == null) {
            return matchingParamValues.get(0);
        }

        Collection<Object> retVal = ReflectionUtil.newInstance(myInnerCollectionType);
        retVal.addAll(matchingParamValues);
        return retVal;
    }

    private void translateQueryParametersIntoServerArgumentForGet(RequestDetails theRequest,
            List<Object> matchingParamValues) {
        if (mySearchParameterBinding != null) {

            List<QualifiedParamList> params = new ArrayList<QualifiedParamList>();
            String nameWithQualifierColon = myName + ":";

            for (String nextParamName : theRequest.getParameters().keySet()) {
                String qualifier;
                if (nextParamName.equals(myName)) {
                    qualifier = null;
                } else if (nextParamName.startsWith(nameWithQualifierColon)) {
                    qualifier = nextParamName.substring(nextParamName.indexOf(':'));
                } else {
                    // This is some other parameter, not the one bound by this instance
                    continue;
                }
                String[] values = theRequest.getParameters().get(nextParamName);
                if (values != null) {
                    for (String nextValue : values) {
                        params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue));
                    }
                }
            }
            if (!params.isEmpty()) {
                for (QualifiedParamList next : params) {
                    Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next));
                    addValueToList(matchingParamValues, values);
                }

            }

        } else {
            String[] paramValues = theRequest.getParameters().get(myName);
            if (paramValues != null && paramValues.length > 0) {
                if (myAllowGet) {

                    if (DateRangeParam.class.isAssignableFrom(myParameterType)) {
                        List<QualifiedParamList> parameters = new ArrayList<QualifiedParamList>();
                        parameters.add(QualifiedParamList.singleton(paramValues[0]));
                        if (paramValues.length > 1) {
                            parameters.add(QualifiedParamList.singleton(paramValues[1]));
                        }
                        DateRangeParam dateRangeParam = new DateRangeParam();
                        FhirContext ctx = theRequest.getServer().getFhirContext();
                        dateRangeParam.setValuesAsQueryTokens(ctx, myName, parameters);
                        matchingParamValues.add(dateRangeParam);
                    } else if (String.class.isAssignableFrom(myParameterType)) {

                        for (String next : paramValues) {
                            matchingParamValues.add(next);
                        }
                    } else if (ValidationModeEnum.class.equals(myParameterType)) {

                        if (isNotBlank(paramValues[0])) {
                            ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]);
                            if (validationMode != null) {
                                matchingParamValues.add(validationMode);
                            } else {
                                throwInvalidMode(paramValues[0]);
                            }
                        }

                    } else {
                        for (String nextValue : paramValues) {
                            FhirContext ctx = theRequest.getServer().getFhirContext();
                            RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) ctx
                                    .getElementDefinition((Class<? extends IBase>) myParameterType);
                            IPrimitiveType<?> instance = def.newInstance();
                            instance.setValueAsString(nextValue);
                            matchingParamValues.add(instance);
                        }
                    }
                } else {
                    HapiLocalizer localizer = theRequest.getServer().getFhirContext().getLocalizer();
                    String msg = localizer.getMessage(OperationParameter.class, "urlParamNotPrimitive",
                            myOperationName, myName);
                    throw new MethodNotAllowedException(msg, RequestTypeEnum.POST);
                }
            }
        }
    }

    private void translateQueryParametersIntoServerArgumentForPost(RequestDetails theRequest,
            List<Object> matchingParamValues) {
        IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY);
        RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents);
        if (def.getName().equals("Parameters")) {

            BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
            BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild
                    .getChildByName("parameter");

            RuntimeChildPrimitiveDatatypeDefinition nameChild = (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem
                    .getChildByName("name");
            BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]");
            BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource");

            IAccessor paramChildAccessor = paramChild.getAccessor();
            List<IBase> values = paramChildAccessor.getValues(requestContents);
            for (IBase nextParameter : values) {
                List<IBase> nextNames = nameChild.getAccessor().getValues(nextParameter);
                if (nextNames != null && nextNames.size() > 0) {
                    IPrimitiveType<?> nextName = (IPrimitiveType<?>) nextNames.get(0);
                    if (myName.equals(nextName.getValueAsString())) {

                        if (myParameterType.isAssignableFrom(nextParameter.getClass())) {
                            matchingParamValues.add(nextParameter);
                        } else {
                            List<IBase> paramValues = valueChild.getAccessor().getValues(nextParameter);
                            List<IBase> paramResources = resourceChild.getAccessor().getValues(nextParameter);
                            if (paramValues != null && paramValues.size() > 0) {
                                tryToAddValues(paramValues, matchingParamValues);
                            } else if (paramResources != null && paramResources.size() > 0) {
                                tryToAddValues(paramResources, matchingParamValues);
                            }
                        }

                    }
                }
            }

        } else {

            if (myParameterType.isAssignableFrom(requestContents.getClass())) {
                tryToAddValues(Arrays.asList((IBase) requestContents), matchingParamValues);
            }

        }
    }

    @SuppressWarnings("unchecked")
    private void tryToAddValues(List<IBase> theParamValues, List<Object> theMatchingParamValues) {
        for (Object nextValue : theParamValues) {
            if (nextValue == null) {
                continue;
            }
            if (myConverter != null) {
                nextValue = myConverter.incomingServer(nextValue);
            }
            if (!myParameterType.isAssignableFrom(nextValue.getClass())) {
                Class<? extends IBaseDatatype> sourceType = (Class<? extends IBaseDatatype>) nextValue.getClass();
                Class<? extends IBaseDatatype> targetType = (Class<? extends IBaseDatatype>) myParameterType;
                BaseRuntimeElementDefinition<?> sourceTypeDef = myContext.getElementDefinition(sourceType);
                BaseRuntimeElementDefinition<?> targetTypeDef = myContext.getElementDefinition(targetType);
                if (targetTypeDef instanceof IRuntimeDatatypeDefinition
                        && sourceTypeDef instanceof IRuntimeDatatypeDefinition) {
                    IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef;
                    if (targetTypeDtDef.isProfileOf(sourceType)) {
                        FhirTerser terser = myContext.newTerser();
                        IBase newTarget = targetTypeDef.newInstance();
                        terser.cloneInto((IBase) nextValue, newTarget, true);
                        theMatchingParamValues.add(newTarget);
                        continue;
                    }
                }
                throwWrongParamType(nextValue);
            }

            addValueToList(theMatchingParamValues, nextValue);
        }
    }

    public static void throwInvalidMode(String paramValues) {
        throw new InvalidRequestException("Invalid mode value: \"" + paramValues + "\"");
    }

    interface IOperationParamConverter {

        Object incomingServer(Object theObject);

        Object outgoingClient(Object theObject);

    }

    class OperationParamConverter implements IOperationParamConverter {

        public OperationParamConverter() {
            Validate.isTrue(mySearchParameterBinding != null);
        }

        @Override
        public Object incomingServer(Object theObject) {
            IPrimitiveType<?> obj = (IPrimitiveType<?>) theObject;
            List<QualifiedParamList> paramList = Collections.singletonList(
                    QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString()));
            return mySearchParameterBinding.parse(myContext, paramList);
        }

        @Override
        public Object outgoingClient(Object theObject) {
            IQueryParameterType obj = (IQueryParameterType) theObject;
            IPrimitiveType<?> retVal = (IPrimitiveType<?>) myContext.getElementDefinition("string").newInstance();
            retVal.setValueAsString(obj.getValueAsQueryToken(myContext));
            return retVal;
        }

    }

}