package ca.uhn.fhir.rest.method; /* * #%L * HAPI FHIR - Core Library * %% * Copyright (C) 2014 - 2017 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.IBaseBundle; 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); } 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()); } 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 BaseHttpClientInvocation createProcessMsgInvocation(FhirContext theContext, String theOperationName, IBaseBundle theInput, Map<String, List<String>> urlParams) { StringBuilder b = new StringBuilder(); if (b.length() > 0) { b.append('/'); } if (!theOperationName.startsWith("$")) { b.append("$"); } b.append(theOperationName); BaseHttpClientInvocation.appendExtraParamsWithQuestionMark(urlParams, b, b.indexOf("?") == -1); return new HttpPostClientInvocation(theContext, theInput, 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; } } }