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 java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.IRestfulResponse;
import ca.uhn.fhir.rest.server.IRestfulServer;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<MethodOutcome> {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class);
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE);
private boolean myReturnVoid;
public BaseOutcomeReturningMethodBinding(Method theMethod, FhirContext theContext, Class<?> theMethodAnnotation, Object theProvider) {
super(theMethod, theContext, theProvider);
if (!theMethod.getReturnType().equals(MethodOutcome.class)) {
if (!allowVoidReturnType()) {
throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " is a @" + theMethodAnnotation.getSimpleName() + " method but it does not return " + MethodOutcome.class);
} else if (theMethod.getReturnType() == void.class) {
myReturnVoid = true;
}
}
}
protected abstract void addParametersForServerRequest(RequestDetails theRequest, Object[] theParams);
/**
* Subclasses may override to allow a void method return type, which is allowable for some methods (e.g. delete)
*/
protected boolean allowVoidReturnType() {
return false;
}
protected abstract BaseHttpClientInvocation createClientInvocation(Object[] theArgs, IResource resource);
/**
* For servers, this method will match only incoming requests that match the given operation, or which have no
* operation in the URL if this method returns null.
*/
protected abstract String getMatchingOperation();
private int getOperationStatus(MethodOutcome response) {
switch (getRestOperationType()) {
case CREATE:
if (response == null) {
throw new InternalErrorException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName() + " returned null, which is not allowed for create operation");
}
if (response.getCreated() == null || Boolean.TRUE.equals(response.getCreated())) {
return Constants.STATUS_HTTP_201_CREATED;
}
return Constants.STATUS_HTTP_200_OK;
case UPDATE:
if (response == null || response.getCreated() == null || Boolean.FALSE.equals(response.getCreated())) {
return Constants.STATUS_HTTP_200_OK;
}
return Constants.STATUS_HTTP_201_CREATED;
case VALIDATE:
case DELETE:
default:
if (response == null) {
if (isReturnVoid() == false) {
throw new InternalErrorException("Method " + getMethod().getName() + " in type " + getMethod().getDeclaringClass().getCanonicalName() + " returned null");
}
return Constants.STATUS_HTTP_204_NO_CONTENT;
}
if (response.getOperationOutcome() == null) {
return Constants.STATUS_HTTP_204_NO_CONTENT;
}
return Constants.STATUS_HTTP_200_OK;
}
}
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
Set<RequestTypeEnum> allowableRequestTypes = provideAllowableRequestTypes();
RequestTypeEnum requestType = theRequest.getRequestType();
if (!allowableRequestTypes.contains(requestType)) {
return false;
}
if (!getResourceName().equals(theRequest.getResourceName())) {
return false;
}
if (getMatchingOperation() == null && StringUtils.isNotBlank(theRequest.getOperation())) {
return false;
}
if (getMatchingOperation() != null && !getMatchingOperation().equals(theRequest.getOperation())) {
return false;
}
/*
* Note: Technically this will match an update (PUT) method even if
* there is no ID in the URL - We allow this here because there is no
* better match for that, and this allows the update/PUT method to give
* a helpful error if the client has forgotten to include the
* ID in the URL.
*
* It's also needed for conditional update..
*/
return true;
}
@Override
public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException {
if (theResponseStatusCode >= 200 && theResponseStatusCode < 300) {
if (myReturnVoid) {
return null;
}
MethodOutcome retVal = MethodUtil.process2xxResponse(getContext(), theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders);
return retVal;
}
throw processNon2xxResponseAndReturnExceptionToThrow(theResponseStatusCode, theResponseMimeType, theResponseReader);
}
@Override
public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
Object[] params = createParametersForServerRequest(theRequest);
addParametersForServerRequest(theRequest, params);
/*
* No need to catch and handle exceptions here, we already handle them one level up including invoking interceptors
* on them
*/
MethodOutcome response;
Object methodReturn = invokeServerMethod(theServer, theRequest, params);
if (methodReturn instanceof IBaseOperationOutcome) {
response = new MethodOutcome();
response.setOperationOutcome((IBaseOperationOutcome) methodReturn);
} else {
response = (MethodOutcome) methodReturn;
}
if (response != null && response.getId() != null && response.getId().hasResourceType()) {
if (getContext().getResourceDefinition(response.getId().getResourceType()) == null) {
throw new InternalErrorException("Server method returned invalid resource ID: " + response.getId().getValue());
}
}
IBaseOperationOutcome outcome = response != null ? response.getOperationOutcome() : null;
IBaseResource resource = response != null ? response.getResource() : null;
return returnResponse(theServer, theRequest, response, outcome, resource);
}
public boolean isReturnVoid() {
return myReturnVoid;
}
protected abstract Set<RequestTypeEnum> provideAllowableRequestTypes();
private Object returnResponse(IRestfulServer<?> theServer, RequestDetails theRequest, MethodOutcome response, IBaseResource originalOutcome, IBaseResource resource) throws IOException {
boolean allowPrefer = false;
int operationStatus = getOperationStatus(response);
IBaseResource outcome = originalOutcome;
if (ourOperationsWhichAllowPreferHeader.contains(getRestOperationType())) {
allowPrefer = true;
}
if (resource != null && allowPrefer) {
String prefer = theRequest.getHeader(Constants.HEADER_PREFER);
PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer);
if (preferReturn != null) {
if (preferReturn == PreferReturnEnum.REPRESENTATION) {
outcome = resource;
}
}
}
for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) {
IServerInterceptor next = theServer.getInterceptors().get(i);
boolean continueProcessing = next.outgoingResponse(theRequest, outcome);
if (!continueProcessing) {
return null;
}
}
IRestfulResponse restfulResponse = theRequest.getResponse();
if (response != null) {
if (response.getResource() != null) {
restfulResponse.setOperationResourceLastUpdated(RestfulServerUtils.extractLastUpdatedFromResource(response.getResource()));
}
IIdType responseId = response.getId();
if (responseId != null && responseId.getResourceType() == null && responseId.hasIdPart()) {
responseId = responseId.withResourceType(getResourceName());
}
if (responseId != null) {
String serverBase = theRequest.getFhirServerBase();
responseId = RestfulServerUtils.fullyQualifyResourceIdOrReturnNull(theServer, resource, serverBase, responseId);
restfulResponse.setOperationResourceId(responseId);
}
}
boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
Set<SummaryEnum> summaryMode = Collections.emptySet();
return restfulResponse.streamResponseAsResource(outcome, prettyPrint, summaryMode, operationStatus, null, theRequest.isRespondGzip(), true);
// return theRequest.getResponse().returnResponse(ParseAction.create(outcome), operationStatus, allowPrefer, response, getResourceName());
}
protected void streamOperationOutcome(BaseServerResponseException theE, RestfulServer theServer, EncodingEnum theEncodingNotNull, HttpServletResponse theResponse, RequestDetails theRequest) throws IOException {
theResponse.setStatus(theE.getStatusCode());
theServer.addHeadersToResponse(theResponse);
if (theE.getOperationOutcome() != null) {
theResponse.setContentType(theEncodingNotNull.getResourceContentType());
IParser parser = theEncodingNotNull.newParser(theServer.getFhirContext());
parser.setPrettyPrint(RestfulServerUtils.prettyPrintResponse(theServer, theRequest));
Writer writer = theResponse.getWriter();
try {
parser.encodeResourceToWriter(theE.getOperationOutcome(), writer);
} finally {
writer.close();
}
} else {
theResponse.setContentType(Constants.CT_TEXT);
Writer writer = theResponse.getWriter();
try {
writer.append(theE.getMessage());
} finally {
writer.close();
}
}
}
protected static void parseContentLocation(FhirContext theContext, MethodOutcome theOutcomeToPopulate, String theLocationHeader) {
if (StringUtils.isBlank(theLocationHeader)) {
return;
}
IIdType id = theContext.getVersion().newIdType();
id.setValue(theLocationHeader);
theOutcomeToPopulate.setId(id);
}
}