All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.uhn.fhir.jpa.graphql.DaoRegistryGraphQLStorageServices Maven / Gradle / Ivy

Go to download

This project provides services useful for fhir repository storage. Actual repository storage services are not in this project, just the api and helper libraries.

The newest version!
/*-
 * #%L
 * HAPI FHIR Storage api
 * %%
 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
 * %%
 * 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%
 */
package ca.uhn.fhir.jpa.graphql;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.BundleLinks;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateOrListParam;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.NumberOrListParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.QuantityOrListParam;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.SpecialOrListParam;
import ca.uhn.fhir.rest.param.SpecialParam;
import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.utilities.graphql.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import static ca.uhn.fhir.rest.api.Constants.PARAM_COUNT;
import static ca.uhn.fhir.rest.api.Constants.PARAM_FILTER;

public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageServices {

	// the constant hasn't already been defined in org.hl7.fhir.core so we define it here
	static final String SEARCH_ID_PARAM = "search-id";
	static final String SEARCH_OFFSET_PARAM = "search-offset";

	private static final int MAX_SEARCH_SIZE = 500;

	@Autowired
	private FhirContext myContext;

	@Autowired
	private DaoRegistry myDaoRegistry;

	@Autowired
	private ISearchParamRegistry mySearchParamRegistry;

	@Autowired
	protected ISearchCoordinatorSvc mySearchCoordinatorSvc;

	@Autowired
	private IRequestPartitionHelperSvc myPartitionHelperSvc;

	@Autowired
	private IPagingProvider myPagingProvider;

	private IFhirResourceDao getDao(String theResourceType) {
		RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(theResourceType);
		return myDaoRegistry.getResourceDaoOrNull(typeDef.getImplementingClass());
	}

	private String graphqlArgumentToSearchParam(String name) {
		if (name.startsWith("_")) {
			return name;
		} else {
			return name.replaceAll("_", "-");
		}
	}

	private String searchParamToGraphqlArgument(String name) {
		return name.replaceAll("-", "_");
	}

	private SearchParameterMap buildSearchParams(String theType, List theSearchParams) {
		List resourceSearchParam = theSearchParams.stream()
				.filter(it -> !PARAM_COUNT.equals(it.getName()))
				.collect(Collectors.toList());

		FhirContext fhirContext = myContext;
		RuntimeResourceDefinition typeDef = fhirContext.getResourceDefinition(theType);

		SearchParameterMap params = new SearchParameterMap();
		ResourceSearchParams searchParams = mySearchParamRegistry.getRuntimeSearchParams(typeDef.getName());

		for (Argument nextArgument : resourceSearchParam) {

			if (nextArgument.getName().equals(PARAM_FILTER)) {
				String value = nextArgument.getValues().get(0).getValue();
				params.add(PARAM_FILTER, new StringParam(value));
				continue;
			}

			String searchParamName = graphqlArgumentToSearchParam(nextArgument.getName());
			RuntimeSearchParam searchParam = searchParams.get(searchParamName);
			if (searchParam == null) {
				Set graphqlArguments = searchParams.getSearchParamNames().stream()
						.map(this::searchParamToGraphqlArgument)
						.collect(Collectors.toSet());
				String msg = myContext
						.getLocalizer()
						.getMessageSanitized(
								DaoRegistryGraphQLStorageServices.class,
								"invalidGraphqlArgument",
								nextArgument.getName(),
								new TreeSet<>(graphqlArguments));
				throw new InvalidRequestException(Msg.code(1275) + msg);
			}

			IQueryParameterOr queryParam;

			switch (searchParam.getParamType()) {
				case NUMBER:
					NumberOrListParam numberOrListParam = new NumberOrListParam();
					for (Value value : nextArgument.getValues()) {
						numberOrListParam.addOr(new NumberParam(value.getValue()));
					}
					queryParam = numberOrListParam;
					break;
				case DATE:
					DateOrListParam dateOrListParam = new DateOrListParam();
					for (Value value : nextArgument.getValues()) {
						dateOrListParam.addOr(new DateParam(value.getValue()));
					}
					queryParam = dateOrListParam;
					break;
				case STRING:
					StringOrListParam stringOrListParam = new StringOrListParam();
					for (Value value : nextArgument.getValues()) {
						stringOrListParam.addOr(new StringParam(value.getValue()));
					}
					queryParam = stringOrListParam;
					break;
				case TOKEN:
					TokenOrListParam tokenOrListParam = new TokenOrListParam();
					for (Value value : nextArgument.getValues()) {
						TokenParam tokenParam = new TokenParam();
						tokenParam.setValueAsQueryToken(fhirContext, searchParamName, null, value.getValue());
						tokenOrListParam.addOr(tokenParam);
					}
					queryParam = tokenOrListParam;
					break;
				case REFERENCE:
					ReferenceOrListParam referenceOrListParam = new ReferenceOrListParam();
					for (Value value : nextArgument.getValues()) {
						referenceOrListParam.addOr(new ReferenceParam(value.getValue()));
					}
					queryParam = referenceOrListParam;
					break;
				case QUANTITY:
					QuantityOrListParam quantityOrListParam = new QuantityOrListParam();
					for (Value value : nextArgument.getValues()) {
						quantityOrListParam.addOr(new QuantityParam(value.getValue()));
					}
					queryParam = quantityOrListParam;
					break;
				case SPECIAL:
					SpecialOrListParam specialOrListParam = new SpecialOrListParam();
					for (Value value : nextArgument.getValues()) {
						specialOrListParam.addOr(new SpecialParam().setValue(value.getValue()));
					}
					queryParam = specialOrListParam;
					break;
				case COMPOSITE:
				case URI:
				case HAS:
				default:
					throw new InvalidRequestException(Msg.code(1276)
							+ String.format(
									"%s parameters are not yet supported in GraphQL", searchParam.getParamType()));
			}

			params.add(searchParamName, queryParam);
		}

		return params;
	}

	@Transactional(propagation = Propagation.NEVER)
	@Override
	public void listResources(
			Object theAppInfo, String theType, List theSearchParams, List theMatches)
			throws FHIRException {
		SearchParameterMap params = buildSearchParams(theType, theSearchParams);
		params.setLoadSynchronousUpTo(MAX_SEARCH_SIZE);

		RequestDetails requestDetails = (RequestDetails) theAppInfo;
		IBundleProvider response = getDao(theType).search(params, requestDetails);
		Integer size = response.size();
		// We set size to null in SearchCoordinatorSvcImpl.executeQuery() if matching results exceeds count
		// so don't throw here
		if ((response.preferredPageSize() != null && size != null && response.preferredPageSize() < size)
				|| size == null) {
			size = response.preferredPageSize();
		}

		Validate.notNull(size, "size is null");
		theMatches.addAll(response.getResources(0, size));
	}

	@Transactional(propagation = Propagation.REQUIRED)
	@Override
	public IBaseResource lookup(Object theAppInfo, String theType, String theId) throws FHIRException {
		IIdType refId = myContext.getVersion().newIdType();
		refId.setValue(theType + "/" + theId);
		return lookup(theAppInfo, refId);
	}

	private IBaseResource lookup(Object theAppInfo, IIdType theRefId) {
		IFhirResourceDao dao = getDao(theRefId.getResourceType());
		RequestDetails requestDetails = (RequestDetails) theAppInfo;
		return dao.read(theRefId, requestDetails, false);
	}

	@Transactional(propagation = Propagation.REQUIRED)
	@Override
	public ReferenceResolution lookup(Object theAppInfo, IBaseResource theContext, IBaseReference theReference)
			throws FHIRException {
		IBaseResource outcome = lookup(theAppInfo, theReference.getReferenceElement());
		if (outcome == null) {
			return null;
		}
		return new ReferenceResolution(theContext, outcome);
	}

	private Optional getArgument(List params, String name) {
		return params.stream()
				.filter(it -> name.equals(it.getName()))
				.map(it -> it.getValues().get(0).getValue())
				.findAny();
	}

	@Transactional(propagation = Propagation.NEVER)
	@Override
	public IBaseBundle search(Object theAppInfo, String theType, List theSearchParams) throws FHIRException {
		RequestDetails requestDetails = (RequestDetails) theAppInfo;

		Optional searchIdArgument = getArgument(theSearchParams, SEARCH_ID_PARAM);
		Optional searchOffsetArgument = getArgument(theSearchParams, SEARCH_OFFSET_PARAM);

		String searchId;
		int searchOffset;
		int pageSize;
		IBundleProvider response;

		if (searchIdArgument.isPresent() && searchOffsetArgument.isPresent()) {
			searchId = searchIdArgument.get();
			searchOffset = Integer.parseInt(searchOffsetArgument.get());

			response = Optional.ofNullable(myPagingProvider.retrieveResultList(requestDetails, searchId))
					.orElseThrow(() -> {
						String msg = myContext
								.getLocalizer()
								.getMessageSanitized(
										DaoRegistryGraphQLStorageServices.class,
										"invalidGraphqlCursorArgument",
										searchId);
						return new InvalidRequestException(Msg.code(2076) + msg);
					});

			pageSize =
					Optional.ofNullable(response.preferredPageSize()).orElseGet(myPagingProvider::getDefaultPageSize);
		} else {
			pageSize = getArgument(theSearchParams, "_count")
					.map(Integer::parseInt)
					.orElseGet(myPagingProvider::getDefaultPageSize);

			SearchParameterMap params = buildSearchParams(theType, theSearchParams);
			params.setCount(pageSize);

			CacheControlDirective cacheControlDirective = new CacheControlDirective();
			cacheControlDirective.parse(requestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL));

			RequestPartitionId requestPartitionId =
					myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(requestDetails, theType, params);
			response = mySearchCoordinatorSvc.registerSearch(
					getDao(theType), params, theType, cacheControlDirective, requestDetails, requestPartitionId);

			searchOffset = 0;
			searchId = myPagingProvider.storeResultList(requestDetails, response);
		}

		// response.size() may return {@literal null}, in that case use pageSize
		String serverBase = requestDetails.getFhirServerBase();
		Optional numTotalResults = Optional.ofNullable(response.size());
		int numToReturn = numTotalResults
				.map(integer -> Math.min(pageSize, integer - searchOffset))
				.orElse(pageSize);

		BundleLinks links = new BundleLinks(
				requestDetails.getServerBaseForRequest(),
				null,
				RestfulServerUtils.prettyPrintResponse(requestDetails.getServer(), requestDetails),
				BundleTypeEnum.SEARCHSET);

		// RestfulServerUtils.createLinkSelf not suitable here
		String linkFormat = "%s/%s?_format=application/json&search-id=%s&search-offset=%d&_count=%d";

		String linkSelf = String.format(linkFormat, serverBase, theType, searchId, searchOffset, pageSize);
		links.setSelf(linkSelf);

		boolean hasNext = numTotalResults
				.map(total -> (searchOffset + numToReturn) < total)
				.orElse(true);

		if (hasNext) {
			String linkNext =
					String.format(linkFormat, serverBase, theType, searchId, searchOffset + numToReturn, pageSize);
			links.setNext(linkNext);
		}

		if (searchOffset > 0) {
			String linkPrev = String.format(
					linkFormat, serverBase, theType, searchId, Math.max(0, searchOffset - pageSize), pageSize);
			links.setPrev(linkPrev);
		}

		List resourceList = response.getResources(searchOffset, numToReturn + searchOffset);

		IVersionSpecificBundleFactory bundleFactory = myContext.newBundleFactory();
		bundleFactory.addRootPropertiesToBundle(response.getUuid(), links, response.size(), response.getPublished());
		bundleFactory.addResourcesToBundle(resourceList, BundleTypeEnum.SEARCHSET, serverBase, null, null);

		IBaseResource result = bundleFactory.getResourceBundle();
		return (IBaseBundle) result;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy