001package ca.uhn.fhir.jpa.graphql; 002 003/*- 004 * #%L 005 * HAPI FHIR Storage api 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.context.RuntimeResourceDefinition; 025import ca.uhn.fhir.context.RuntimeSearchParam; 026import ca.uhn.fhir.i18n.Msg; 027import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 028import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 029import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 030import ca.uhn.fhir.model.api.IQueryParameterOr; 031import ca.uhn.fhir.rest.api.server.IBundleProvider; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.rest.param.DateOrListParam; 034import ca.uhn.fhir.rest.param.DateParam; 035import ca.uhn.fhir.rest.param.NumberOrListParam; 036import ca.uhn.fhir.rest.param.NumberParam; 037import ca.uhn.fhir.rest.param.QuantityOrListParam; 038import ca.uhn.fhir.rest.param.QuantityParam; 039import ca.uhn.fhir.rest.param.ReferenceOrListParam; 040import ca.uhn.fhir.rest.param.ReferenceParam; 041import ca.uhn.fhir.rest.param.SpecialOrListParam; 042import ca.uhn.fhir.rest.param.SpecialParam; 043import ca.uhn.fhir.rest.param.StringOrListParam; 044import ca.uhn.fhir.rest.param.StringParam; 045import ca.uhn.fhir.rest.param.TokenOrListParam; 046import ca.uhn.fhir.rest.param.TokenParam; 047import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 048import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; 049import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 050import org.apache.commons.lang3.Validate; 051import org.hl7.fhir.exceptions.FHIRException; 052import org.hl7.fhir.instance.model.api.IBaseBundle; 053import org.hl7.fhir.instance.model.api.IBaseReference; 054import org.hl7.fhir.instance.model.api.IBaseResource; 055import org.hl7.fhir.instance.model.api.IIdType; 056import org.hl7.fhir.utilities.graphql.Argument; 057import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; 058import org.hl7.fhir.utilities.graphql.Value; 059import org.springframework.beans.factory.annotation.Autowired; 060import org.springframework.transaction.annotation.Propagation; 061import org.springframework.transaction.annotation.Transactional; 062 063import java.util.List; 064import java.util.Map; 065import java.util.Set; 066import java.util.TreeSet; 067import java.util.stream.Collectors; 068 069import static ca.uhn.fhir.rest.api.Constants.PARAM_FILTER; 070 071public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageServices { 072 073 private static final int MAX_SEARCH_SIZE = 500; 074 @Autowired 075 private FhirContext myContext; 076 @Autowired 077 private DaoRegistry myDaoRegistry; 078 @Autowired 079 private ISearchParamRegistry mySearchParamRegistry; 080 081 private IFhirResourceDao<? extends IBaseResource> getDao(String theResourceType) { 082 RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(theResourceType); 083 return myDaoRegistry.getResourceDaoOrNull(typeDef.getImplementingClass()); 084 } 085 086 private String graphqlArgumentToSearchParam(String name) { 087 if (name.startsWith("_")) { 088 return name; 089 } else { 090 return name.replaceAll("_", "-"); 091 } 092 } 093 094 private String searchParamToGraphqlArgument(String name) { 095 return name.replaceAll("-", "_"); 096 } 097 098 @Transactional(propagation = Propagation.NEVER) 099 @Override 100 public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IBaseResource> theMatches) throws FHIRException { 101 FhirContext fhirContext = myContext; 102 RuntimeResourceDefinition typeDef = fhirContext.getResourceDefinition(theType); 103 IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(typeDef.getImplementingClass()); 104 105 SearchParameterMap params = new SearchParameterMap(); 106 params.setLoadSynchronousUpTo(MAX_SEARCH_SIZE); 107 108 Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(typeDef.getName()); 109 110 for (Argument nextArgument : theSearchParams) { 111 112 if (nextArgument.getName().equals(PARAM_FILTER)) { 113 String value = nextArgument.getValues().get(0).getValue(); 114 params.add(PARAM_FILTER, new StringParam(value)); 115 continue; 116 } 117 118 String searchParamName = graphqlArgumentToSearchParam(nextArgument.getName()); 119 RuntimeSearchParam searchParam = searchParams.get(searchParamName); 120 if (searchParam == null) { 121 Set<String> graphqlArguments = searchParams.keySet().stream() 122 .map(this::searchParamToGraphqlArgument) 123 .collect(Collectors.toSet()); 124 String msg = myContext.getLocalizer().getMessageSanitized(DaoRegistryGraphQLStorageServices.class, "invalidGraphqlArgument", nextArgument.getName(), new TreeSet<>(graphqlArguments)); 125 throw new InvalidRequestException(Msg.code(1275) + msg); 126 } 127 128 IQueryParameterOr<?> queryParam; 129 130 switch (searchParam.getParamType()) { 131 case NUMBER: 132 NumberOrListParam numberOrListParam = new NumberOrListParam(); 133 for (Value value : nextArgument.getValues()) { 134 numberOrListParam.addOr(new NumberParam(value.getValue())); 135 } 136 queryParam = numberOrListParam; 137 break; 138 case DATE: 139 DateOrListParam dateOrListParam = new DateOrListParam(); 140 for (Value value : nextArgument.getValues()) { 141 dateOrListParam.addOr(new DateParam(value.getValue())); 142 } 143 queryParam = dateOrListParam; 144 break; 145 case STRING: 146 StringOrListParam stringOrListParam = new StringOrListParam(); 147 for (Value value : nextArgument.getValues()) { 148 stringOrListParam.addOr(new StringParam(value.getValue())); 149 } 150 queryParam = stringOrListParam; 151 break; 152 case TOKEN: 153 TokenOrListParam tokenOrListParam = new TokenOrListParam(); 154 for (Value value : nextArgument.getValues()) { 155 TokenParam tokenParam = new TokenParam(); 156 tokenParam.setValueAsQueryToken(fhirContext, searchParamName, null, value.getValue()); 157 tokenOrListParam.addOr(tokenParam); 158 } 159 queryParam = tokenOrListParam; 160 break; 161 case REFERENCE: 162 ReferenceOrListParam referenceOrListParam = new ReferenceOrListParam(); 163 for (Value value : nextArgument.getValues()) { 164 referenceOrListParam.addOr(new ReferenceParam(value.getValue())); 165 } 166 queryParam = referenceOrListParam; 167 break; 168 case QUANTITY: 169 QuantityOrListParam quantityOrListParam = new QuantityOrListParam(); 170 for (Value value : nextArgument.getValues()) { 171 quantityOrListParam.addOr(new QuantityParam(value.getValue())); 172 } 173 queryParam = quantityOrListParam; 174 break; 175 case SPECIAL: 176 SpecialOrListParam specialOrListParam = new SpecialOrListParam(); 177 for (Value value : nextArgument.getValues()) { 178 specialOrListParam.addOr(new SpecialParam().setValue(value.getValue())); 179 } 180 queryParam = specialOrListParam; 181 break; 182 case COMPOSITE: 183 case URI: 184 case HAS: 185 default: 186 throw new InvalidRequestException(Msg.code(1276) + String.format("%s parameters are not yet supported in GraphQL", searchParam.getParamType())); 187 } 188 189 params.add(searchParamName, queryParam); 190 } 191 192 RequestDetails requestDetails = (RequestDetails) theAppInfo; 193 IBundleProvider response = dao.search(params, requestDetails); 194 Integer size = response.size(); 195 //We set size to null in SearchCoordinatorSvcImpl.executeQuery() if matching results exceeds count 196 //so don't throw here 197 if ((response.preferredPageSize() != null && size != null && response.preferredPageSize() < size) || 198 size == null) { 199 size = response.preferredPageSize(); 200 } 201 202 Validate.notNull(size, "size is null"); 203 theMatches.addAll(response.getResources(0, size)); 204 205 } 206 207 @Transactional(propagation = Propagation.REQUIRED) 208 @Override 209 public IBaseResource lookup(Object theAppInfo, String theType, String theId) throws FHIRException { 210 IIdType refId = myContext.getVersion().newIdType(); 211 refId.setValue(theType + "/" + theId); 212 return lookup(theAppInfo, refId); 213 } 214 215 private IBaseResource lookup(Object theAppInfo, IIdType theRefId) { 216 IFhirResourceDao<? extends IBaseResource> dao = getDao(theRefId.getResourceType()); 217 RequestDetails requestDetails = (RequestDetails) theAppInfo; 218 return dao.read(theRefId, requestDetails, false); 219 } 220 221 @Transactional(propagation = Propagation.REQUIRED) 222 @Override 223 public ReferenceResolution lookup(Object theAppInfo, IBaseResource theContext, IBaseReference theReference) throws FHIRException { 224 IBaseResource outcome = lookup(theAppInfo, theReference.getReferenceElement()); 225 if (outcome == null) { 226 return null; 227 } 228 return new ReferenceResolution(theContext, outcome); 229 } 230 231 @Transactional(propagation = Propagation.NEVER) 232 @Override 233 public IBaseBundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException { 234 throw new NotImplementedOperationException(Msg.code(1277) + "Not yet able to handle this GraphQL request"); 235 } 236 237}