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