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

Java tutorial

Introduction

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

Source

package ca.uhn.fhir.rest.method;

/*
 * #%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 static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;

import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.param.ResourceParameter;
import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.IRestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.FhirTerser;

public class OperationMethodBinding extends BaseResourceReturningMethodBinding {

    private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class);
    private BundleTypeEnum myBundleType;
    private boolean myCanOperateAtInstanceLevel;
    private boolean myCanOperateAtServerLevel;
    private boolean myCanOperateAtTypeLevel;
    private String myDescription;
    private final boolean myIdempotent;
    private final Integer myIdParamIndex;
    private final String myName;
    private final RestOperationTypeEnum myOtherOperatiopnType;
    private List<ReturnType> myReturnParams;
    private final ReturnTypeEnum myReturnType;

    protected OperationMethodBinding(Class<?> theReturnResourceType,
            Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext,
            Object theProvider, boolean theIdempotent, String theOperationName,
            Class<? extends IBaseResource> theOperationType, OperationParam[] theReturnParams,
            BundleTypeEnum theBundleType) {
        super(theReturnResourceType, theMethod, theContext, theProvider);

        myBundleType = theBundleType;
        myIdempotent = theIdempotent;
        myIdParamIndex = MethodUtil.findIdParameterIndex(theMethod, getContext());
        if (myIdParamIndex != null) {
            for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) {
                if (next instanceof IdParam) {
                    myCanOperateAtTypeLevel = ((IdParam) next).optional() == true;
                }
            }
        } else {
            myCanOperateAtTypeLevel = true;
        }

        Description description = theMethod.getAnnotation(Description.class);
        if (description != null) {
            myDescription = description.formalDefinition();
            if (isBlank(myDescription)) {
                myDescription = description.shortDefinition();
            }
        }
        if (isBlank(myDescription)) {
            myDescription = null;
        }

        if (isBlank(theOperationName)) {
            throw new ConfigurationException("Method '" + theMethod.getName() + "' on type "
                    + theMethod.getDeclaringClass().getName() + " is annotated with @"
                    + Operation.class.getSimpleName() + " but this annotation has no name defined");
        }
        if (theOperationName.startsWith("$") == false) {
            theOperationName = "$" + theOperationName;
        }
        myName = theOperationName;

        if (theContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU1)) {
            throw new ConfigurationException("@" + Operation.class.getSimpleName()
                    + " methods are not supported on servers for FHIR version "
                    + theContext.getVersion().getVersion().name() + " - Found one on class "
                    + theMethod.getDeclaringClass().getName());
        }

        if (theReturnTypeFromRp != null) {
            setResourceName(theContext.getResourceDefinition(theReturnTypeFromRp).getName());
        } else {
            if (Modifier.isAbstract(theOperationType.getModifiers()) == false) {
                setResourceName(theContext.getResourceDefinition(theOperationType).getName());
            } else {
                setResourceName(null);
            }
        }

        if (theMethod.getReturnType().isAssignableFrom(Bundle.class)) {
            throw new ConfigurationException("Can not return a DSTU1 bundle from an @"
                    + Operation.class.getSimpleName() + " method. Found in method " + theMethod.getName()
                    + " defined in type " + theMethod.getDeclaringClass().getName());
        }

        if (theMethod.getReturnType().equals(IBundleProvider.class)) {
            myReturnType = ReturnTypeEnum.BUNDLE;
        } else {
            myReturnType = ReturnTypeEnum.RESOURCE;
        }

        if (getResourceName() == null) {
            myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
        } else if (myIdParamIndex == null) {
            myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
        } else {
            myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
        }

        myReturnParams = new ArrayList<OperationMethodBinding.ReturnType>();
        if (theReturnParams != null) {
            for (OperationParam next : theReturnParams) {
                ReturnType type = new ReturnType();
                type.setName(next.name());
                type.setMin(next.min());
                type.setMax(next.max());
                if (type.getMax() == OperationParam.MAX_DEFAULT) {
                    type.setMax(1);
                }
                if (!next.type().equals(IBase.class)) {
                    if (next.type().isInterface() || Modifier.isAbstract(next.type().getModifiers())) {
                        throw new ConfigurationException(
                                "Invalid value for @OperationParam.type(): " + next.type().getName());
                    }
                    type.setType(theContext.getElementDefinition(next.type()).getName());
                }
                myReturnParams.add(type);
            }
        }

        if (myIdParamIndex != null) {
            myCanOperateAtInstanceLevel = true;
        }
        if (getResourceName() == null) {
            myCanOperateAtServerLevel = true;
        }

    }

    public OperationMethodBinding(Class<?> theReturnResourceType,
            Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext,
            Object theProvider, Operation theAnnotation) {
        this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider,
                theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(),
                theAnnotation.returnParameters(), theAnnotation.bundleType());
    }

    public String getDescription() {
        return myDescription;
    }

    /**
     * Returns the name of the operation, starting with "$"
     */
    public String getName() {
        return myName;
    }

    @Override
    protected BundleTypeEnum getResponseBundleType() {
        return myBundleType;
    }

    @Override
    public RestOperationTypeEnum getRestOperationType() {
        return myOtherOperatiopnType;
    }

    public List<ReturnType> getReturnParams() {
        return Collections.unmodifiableList(myReturnParams);
    }

    @Override
    public ReturnTypeEnum getReturnType() {
        return myReturnType;
    }

    @Override
    public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
        if (getResourceName() == null) {
            if (isNotBlank(theRequest.getResourceName())) {
                return false;
            }
        } else if (!getResourceName().equals(theRequest.getResourceName())) {
            return false;
        }

        if (!myName.equals(theRequest.getOperation())) {
            return false;
        }

        RequestTypeEnum requestType = theRequest.getRequestType();
        if (requestType != RequestTypeEnum.GET && requestType != RequestTypeEnum.POST) {
            // Operations can only be invoked with GET and POST
            return false;
        }

        boolean requestHasId = theRequest.getId() != null;
        if (requestHasId) {
            if (isCanOperateAtInstanceLevel() == false) {
                return false;
            }
        } else {
            if (myCanOperateAtTypeLevel == false) {
                return false;
            }
        }

        return true;
    }

    @Override
    public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
        String id = null;
        if (myIdParamIndex != null) {
            IIdType idDt = (IIdType) theArgs[myIdParamIndex];
            id = idDt.getValue();
        }
        IBaseParameters parameters = (IBaseParameters) getContext().getResourceDefinition("Parameters")
                .newInstance();

        if (theArgs != null) {
            for (int idx = 0; idx < theArgs.length; idx++) {
                IParameter nextParam = getParameters().get(idx);
                nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, parameters);
            }
        }

        return createOperationInvocation(getContext(), getResourceName(), id, myName, parameters, false);
    }

    @Override
    public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest)
            throws BaseServerResponseException, IOException {
        if (theRequest.getRequestType() == RequestTypeEnum.POST) {
            IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null);
            theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents);
        }
        return super.invokeServer(theServer, theRequest);
    }

    @Override
    public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams)
            throws BaseServerResponseException {
        if (theRequest.getRequestType() == RequestTypeEnum.POST) {
            // all good
        } else if (theRequest.getRequestType() == RequestTypeEnum.GET) {
            if (!myIdempotent) {
                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class,
                        "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
                throw new MethodNotAllowedException(message, RequestTypeEnum.POST);
            }
        } else {
            if (!myIdempotent) {
                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class,
                        "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
                throw new MethodNotAllowedException(message, RequestTypeEnum.POST);
            } else {
                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class,
                        "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.GET.name(),
                        RequestTypeEnum.POST.name());
                throw new MethodNotAllowedException(message, RequestTypeEnum.GET, RequestTypeEnum.POST);
            }
        }

        if (myIdParamIndex != null) {
            theMethodParams[myIdParamIndex] = theRequest.getId();
        }

        Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
        IBundleProvider retVal = toResourceList(response);
        return retVal;
    }

    public boolean isCanOperateAtInstanceLevel() {
        return this.myCanOperateAtInstanceLevel;
    }

    public boolean isCanOperateAtServerLevel() {
        return this.myCanOperateAtServerLevel;
    }

    public boolean isCanOperateAtTypeLevel() {
        return myCanOperateAtTypeLevel;
    }

    public boolean isIdempotent() {
        return myIdempotent;
    }

    @Override
    protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails,
            ActionRequestDetails theDetails, Object[] theMethodParams) {
        super.populateActionRequestDetailsForInterceptor(theRequestDetails, theDetails, theMethodParams);
        theDetails.setResource((IBaseResource) theRequestDetails.getUserData()
                .get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY));
    }

    public void setDescription(String theDescription) {
        myDescription = theDescription;
    }

    public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName,
            String theId, String theOperationName, IBaseParameters theInput, boolean theUseHttpGet) {
        StringBuilder b = new StringBuilder();
        if (theResourceName != null) {
            b.append(theResourceName);
            if (isNotBlank(theId)) {
                b.append('/');
                b.append(theId);
            }
        }
        if (b.length() > 0) {
            b.append('/');
        }
        if (!theOperationName.startsWith("$")) {
            b.append("$");
        }
        b.append(theOperationName);

        if (!theUseHttpGet) {
            return new HttpPostClientInvocation(theContext, theInput, b.toString());
        } else {
            FhirTerser t = theContext.newTerser();
            List<Object> parameters = t.getValues(theInput, "Parameters.parameter");

            Map<String, List<String>> params = new LinkedHashMap<String, List<String>>();
            for (Object nextParameter : parameters) {
                IPrimitiveType<?> nextNameDt = (IPrimitiveType<?>) t.getSingleValueOrNull((IBase) nextParameter,
                        "name");
                if (nextNameDt == null || nextNameDt.isEmpty()) {
                    ourLog.warn(
                            "Ignoring input parameter with no value in Parameters.parameter.name in operation client invocation");
                    continue;
                }
                String nextName = nextNameDt.getValueAsString();
                if (!params.containsKey(nextName)) {
                    params.put(nextName, new ArrayList<String>());
                }

                IBaseDatatype value = (IBaseDatatype) t.getSingleValueOrNull((IBase) nextParameter, "value[x]");
                if (value == null) {
                    continue;
                }
                if (!(value instanceof IPrimitiveType)) {
                    throw new IllegalArgumentException(
                            "Can not invoke operation as HTTP GET when it has parameters with a composite (non priitive) datatype as the value. Found value: "
                                    + value.getClass().getName());
                }
                IPrimitiveType<?> primitive = (IPrimitiveType<?>) value;
                params.get(nextName).add(primitive.getValueAsString());
            }
            return new HttpGetClientInvocation(theContext, params, b.toString());
        }
    }

    public static class ReturnType {
        private int myMax;
        private int myMin;
        private String myName;
        /**
         * http://hl7-fhir.github.io/valueset-operation-parameter-type.html
         */
        private String myType;

        public int getMax() {
            return myMax;
        }

        public int getMin() {
            return myMin;
        }

        public String getName() {
            return myName;
        }

        public String getType() {
            return myType;
        }

        public void setMax(int theMax) {
            myMax = theMax;
        }

        public void setMin(int theMin) {
            myMin = theMin;
        }

        public void setName(String theName) {
            myName = theName;
        }

        public void setType(String theType) {
            myType = theType;
        }
    }

}