
001package ca.uhn.fhir.rest.server.method; 002 003/*- 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.Pointcut; 026import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; 027import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; 028import ca.uhn.fhir.rest.api.Constants; 029import ca.uhn.fhir.rest.api.RequestTypeEnum; 030import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 031import ca.uhn.fhir.rest.api.server.IRestfulServer; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.rest.api.server.ResponseDetails; 034import ca.uhn.fhir.rest.param.ParameterUtil; 035import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 036import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 037import org.apache.commons.lang3.Validate; 038import org.hl7.fhir.instance.model.api.IBaseResource; 039 040import javax.annotation.Nonnull; 041import javax.servlet.http.HttpServletRequest; 042import javax.servlet.http.HttpServletResponse; 043import java.io.IOException; 044import java.io.Writer; 045import java.lang.reflect.Method; 046import java.util.Collections; 047import java.util.Set; 048 049public class GraphQLMethodBinding extends OperationMethodBinding { 050 051 private final Integer myIdParamIndex; 052 private final Integer myQueryUrlParamIndex; 053 private final Integer myQueryBodyParamIndex; 054 private final RequestTypeEnum myMethodRequestType; 055 056 public GraphQLMethodBinding(Method theMethod, RequestTypeEnum theMethodRequestType, FhirContext theContext, Object theProvider) { 057 super(null, null, theMethod, theContext, theProvider, true, Constants.OPERATION_NAME_GRAPHQL, null, null, null, null, true); 058 059 myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, theContext); 060 myQueryUrlParamIndex = ParameterUtil.findParamAnnotationIndex(theMethod, GraphQLQueryUrl.class); 061 myQueryBodyParamIndex = ParameterUtil.findParamAnnotationIndex(theMethod, GraphQLQueryBody.class); 062 myMethodRequestType = theMethodRequestType; 063 } 064 065 @Override 066 public String getResourceName() { 067 return null; 068 } 069 070 @Nonnull 071 @Override 072 public RestOperationTypeEnum getRestOperationType() { 073 return RestOperationTypeEnum.GRAPHQL_REQUEST; 074 } 075 076 @Override 077 public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) { 078 return getRestOperationType(); 079 } 080 081 @Override 082 protected Set<Class<?>> provideExpectedReturnTypes() { 083 return Collections.singleton(String.class); 084 } 085 086 @Override 087 public boolean isCanOperateAtServerLevel() { 088 return true; 089 } 090 091 @Override 092 public boolean isCanOperateAtTypeLevel() { 093 return false; 094 } 095 096 @Override 097 public boolean isCanOperateAtInstanceLevel() { 098 return myIdParamIndex != null; 099 } 100 101 @Override 102 public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { 103 if (Constants.OPERATION_NAME_GRAPHQL.equals(theRequest.getOperation()) && myMethodRequestType.equals(theRequest.getRequestType())) { 104 return MethodMatchEnum.EXACT; 105 } 106 107 return MethodMatchEnum.NONE; 108 } 109 110 private String getQueryValue(Object[] methodParams) { 111 switch (myMethodRequestType) { 112 case POST: 113 Validate.notNull(myQueryBodyParamIndex, "GraphQL method does not have @" + GraphQLQueryBody.class.getSimpleName() + " parameter"); 114 return (String) methodParams[myQueryBodyParamIndex]; 115 case GET: 116 Validate.notNull(myQueryUrlParamIndex, "GraphQL method does not have @" + GraphQLQueryUrl.class.getSimpleName() + " parameter"); 117 return (String) methodParams[myQueryUrlParamIndex]; 118 } 119 return null; 120 } 121 122 @Override 123 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { 124 Object[] methodParams = createMethodParams(theRequest); 125 if (myIdParamIndex != null) { 126 methodParams[myIdParamIndex] = theRequest.getId(); 127 } 128 129 String responseString = (String) invokeServerMethod(theRequest, methodParams); 130 131 int statusCode = Constants.STATUS_HTTP_200_OK; 132 String statusMessage = Constants.HTTP_STATUS_NAMES.get(statusCode); 133 String contentType = Constants.CT_JSON; 134 String charset = Constants.CHARSET_NAME_UTF8; 135 boolean respondGzip = theRequest.isRespondGzip(); 136 137 HttpServletRequest servletRequest = null; 138 HttpServletResponse servletResponse = null; 139 if (theRequest instanceof ServletRequestDetails) { 140 servletRequest = ((ServletRequestDetails) theRequest).getServletRequest(); 141 servletResponse = ((ServletRequestDetails) theRequest).getServletResponse(); 142 } 143 144 String graphQLQuery = getQueryValue(methodParams); 145 // Interceptor call: SERVER_OUTGOING_GRAPHQL_RESPONSE 146 HookParams params = new HookParams() 147 .add(RequestDetails.class, theRequest) 148 .addIfMatchesType(ServletRequestDetails.class, theRequest) 149 .add(String.class, graphQLQuery) 150 .add(String.class, responseString) 151 .add(HttpServletRequest.class, servletRequest) 152 .add(HttpServletResponse.class, servletResponse); 153 if (!theRequest.getInterceptorBroadcaster().callHooks(Pointcut.SERVER_OUTGOING_GRAPHQL_RESPONSE, params)) { 154 return null; 155 } 156 157 // Interceptor call: SERVER_OUTGOING_RESPONSE 158 params = new HookParams() 159 .add(RequestDetails.class, theRequest) 160 .addIfMatchesType(ServletRequestDetails.class, theRequest) 161 .add(IBaseResource.class, null) 162 .add(ResponseDetails.class, new ResponseDetails()) 163 .add(HttpServletRequest.class, servletRequest) 164 .add(HttpServletResponse.class, servletResponse); 165 if (!theRequest.getInterceptorBroadcaster().callHooks(Pointcut.SERVER_OUTGOING_RESPONSE, params)) { 166 return null; 167 } 168 169 // Write the response 170 Writer writer = theRequest.getResponse().getResponseWriter(statusCode, statusMessage, contentType, charset, respondGzip); 171 writer.write(responseString); 172 writer.close(); 173 174 return null; 175 } 176}