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

ca.uhn.fhir.jpa.dao.validation.SearchParameterDaoValidator 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.dao.validation;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.ElementUtil;
import ca.uhn.fhir.util.HapiExtensions;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r5.model.Enumerations;
import org.hl7.fhir.r5.model.SearchParameter;

import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.DATE;
import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.NUMBER;
import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.QUANTITY;
import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.REFERENCE;
import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.STRING;
import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.TOKEN;
import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.URI;
import static org.apache.commons.lang3.StringUtils.isBlank;

public class SearchParameterDaoValidator {

	private static final Pattern REGEX_SP_EXPRESSION_HAS_PATH = Pattern.compile("[( ]*([A-Z][a-zA-Z]+\\.)?[a-z].*");

	private final FhirContext myFhirContext;
	private final JpaStorageSettings myStorageSettings;
	private final ISearchParamRegistry mySearchParamRegistry;

	public SearchParameterDaoValidator(
			FhirContext theContext,
			JpaStorageSettings theStorageSettings,
			ISearchParamRegistry theSearchParamRegistry) {
		myFhirContext = theContext;
		myStorageSettings = theStorageSettings;
		mySearchParamRegistry = theSearchParamRegistry;
	}

	public void validate(SearchParameter searchParameter) {
		/*
		 * If overriding built-in SPs is disabled on this server, make sure we aren't
		 * doing that
		 */
		if (myStorageSettings.isDefaultSearchParamsCanBeOverridden() == false) {
			for (IPrimitiveType nextBaseType : searchParameter.getBase()) {
				String nextBase = nextBaseType.getValueAsString();
				RuntimeSearchParam existingSearchParam =
						mySearchParamRegistry.getActiveSearchParam(nextBase, searchParameter.getCode());
				if (existingSearchParam != null) {
					boolean isBuiltIn = existingSearchParam.getId() == null;
					isBuiltIn |= existingSearchParam.getUri().startsWith("http://hl7.org/fhir/SearchParameter/");
					if (isBuiltIn) {
						throw new UnprocessableEntityException(
								Msg.code(1111) + "Can not override built-in search parameter " + nextBase + ":"
										+ searchParameter.getCode() + " because overriding is disabled on this server");
					}
				}
			}
		}

		/*
		 * Everything below is validating that the SP is actually valid. We'll only do that if the
		 * SPO is active, so that we don't block people from uploading works-in-progress
		 */
		if (searchParameter.getStatus() == null) {
			throw new UnprocessableEntityException(Msg.code(1112) + "SearchParameter.status is missing or invalid");
		}
		if (!searchParameter.getStatus().name().equals("ACTIVE")) {
			return;
		}

		// Search parameters must have a base
		if (isCompositeWithoutBase(searchParameter)) {
			throw new UnprocessableEntityException(Msg.code(1113) + "SearchParameter.base is missing");
		}

		// Do we have a valid expression
		if (isCompositeWithoutExpression(searchParameter)) {

			// this is ok

		} else if (isBlank(searchParameter.getExpression())) {

			throw new UnprocessableEntityException(Msg.code(1114) + "SearchParameter.expression is missing");

		} else {

			FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion();
			if (fhirVersion.isOlderThan(FhirVersionEnum.DSTU3)) {
				// omitting validation for DSTU2_HL7ORG, DSTU2_1 and DSTU2
			} else {
				maybeValidateCompositeSpForUniqueIndexing(searchParameter);
				maybeValidateSearchParameterExpressionsOnSave(searchParameter);
				maybeValidateCompositeWithComponent(searchParameter);
			}
		}
	}

	private boolean isCompositeSp(SearchParameter theSearchParameter) {
		return theSearchParameter.getType() != null
				&& theSearchParameter.getType().equals(Enumerations.SearchParamType.COMPOSITE);
	}

	private boolean isCompositeWithoutBase(SearchParameter searchParameter) {
		return ElementUtil.isEmpty(searchParameter.getBase())
				&& ElementUtil.isEmpty(
						searchParameter.getExtensionsByUrl(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE))
				&& !isCompositeSp(searchParameter);
	}

	private boolean isCompositeWithoutExpression(SearchParameter searchParameter) {
		return isCompositeSp(searchParameter) && isBlank(searchParameter.getExpression());
	}

	private boolean isCompositeWithComponent(SearchParameter theSearchParameter) {
		return isCompositeSp(theSearchParameter) && theSearchParameter.hasComponent();
	}

	private boolean isCompositeSpForUniqueIndexing(SearchParameter theSearchParameter) {
		return isCompositeSp(theSearchParameter) && hasAnyExtensionUniqueSetTo(theSearchParameter, true);
	}

	private void maybeValidateCompositeSpForUniqueIndexing(SearchParameter theSearchParameter) {
		if (isCompositeSpForUniqueIndexing(theSearchParameter)) {
			if (!theSearchParameter.hasComponent()) {
				throw new UnprocessableEntityException(
						Msg.code(1115) + "SearchParameter is marked as unique but has no components");
			}
			for (SearchParameter.SearchParameterComponentComponent next : theSearchParameter.getComponent()) {
				if (isBlank(next.getDefinition())) {
					throw new UnprocessableEntityException(
							Msg.code(1116) + "SearchParameter is marked as unique but is missing component.definition");
				}
			}
		}
	}

	private void maybeValidateSearchParameterExpressionsOnSave(SearchParameter theSearchParameter) {
		if (myStorageSettings.isValidateSearchParameterExpressionsOnSave()) {
			validateExpressionPath(theSearchParameter);
			validateExpressionIsParsable(theSearchParameter);
		}
	}

	private void validateExpressionPath(SearchParameter theSearchParameter) {
		String expression = getExpression(theSearchParameter);

		boolean isResourceOfTypeComposite = theSearchParameter.getType() == Enumerations.SearchParamType.COMPOSITE;
		boolean isResourceOfTypeSpecial = theSearchParameter.getType() == Enumerations.SearchParamType.SPECIAL;
		boolean expressionHasPath =
				REGEX_SP_EXPRESSION_HAS_PATH.matcher(expression).matches();

		boolean isUnique = hasAnyExtensionUniqueSetTo(theSearchParameter, true);

		if (!isUnique && !isResourceOfTypeComposite && !isResourceOfTypeSpecial && !expressionHasPath) {
			throw new UnprocessableEntityException(Msg.code(1120) + "SearchParameter.expression value \"" + expression
					+ "\" is invalid due to missing/incorrect path");
		}
	}

	private void validateExpressionIsParsable(SearchParameter theSearchParameter) {
		String expression = getExpression(theSearchParameter);

		try {
			myFhirContext.newFhirPath().parse(expression);
		} catch (Exception exception) {
			throw new UnprocessableEntityException(
					Msg.code(1121) + "Invalid FHIRPath format for SearchParameter.expression \"" + expression + "\": "
							+ exception.getMessage());
		}
	}

	private String getExpression(SearchParameter theSearchParameter) {
		return theSearchParameter.getExpression().trim();
	}

	private boolean hasAnyExtensionUniqueSetTo(SearchParameter theSearchParameter, boolean theValue) {
		String theValueAsString = Boolean.toString(theValue);

		return theSearchParameter.getExtensionsByUrl(HapiExtensions.EXT_SP_UNIQUE).stream()
				.anyMatch(t -> theValueAsString.equals(t.getValueAsPrimitive().getValueAsString()));
	}

	private void maybeValidateCompositeWithComponent(SearchParameter theSearchParameter) {
		if (isCompositeWithComponent(theSearchParameter)) {
			validateCompositeSearchParameterComponents(theSearchParameter);
		}
	}

	private void validateCompositeSearchParameterComponents(SearchParameter theSearchParameter) {
		theSearchParameter.getComponent().stream()
				.filter(SearchParameter.SearchParameterComponentComponent::hasDefinition)
				.map(SearchParameter.SearchParameterComponentComponent::getDefinition)
				.filter(Objects::nonNull)
				.map(mySearchParamRegistry::getActiveSearchParamByUrl)
				.filter(Objects::nonNull)
				.forEach(theRuntimeSp -> validateComponentSpTypeAgainstWhiteList(
						theRuntimeSp, getAllowedSearchParameterTypes(theSearchParameter)));
	}

	private void validateComponentSpTypeAgainstWhiteList(
			RuntimeSearchParam theRuntimeSearchParam,
			Collection theAllowedSearchParamTypes) {
		if (!theAllowedSearchParamTypes.contains(theRuntimeSearchParam.getParamType())) {
			throw new UnprocessableEntityException(String.format(
					"%sInvalid component search parameter type: %s in component.definition: %s, supported types: %s",
					Msg.code(2347),
					theRuntimeSearchParam.getParamType().name(),
					theRuntimeSearchParam.getUri(),
					theAllowedSearchParamTypes.stream().map(Enum::name).collect(Collectors.joining(", "))));
		}
	}

	/*
	 * Returns allowed Search Parameter Types for a given composite or combo search parameter
	 * This prevents the creation of search parameters that would fail during runtime (during a GET request)
	 * Below you can find references to runtime usage for each parameter type:
	 *
	 * For Composite Search Parameters without HSearch indexing enabled (JPA only):
	 * @see QueryStack#createPredicateCompositePart() and SearchBuilder#createCompositeSort()
	 *
	 * For Composite Search Parameters with HSearch indexing enabled:
	 * @see HSearchCompositeSearchIndexDataImpl#writeIndexEntry()
	 *
	 * For Combo Search Parameters:
	 * @see BaseSearchParamExtractor.extractParameterCombinationsForComboParam()
	 */
	private Set getAllowedSearchParameterTypes(SearchParameter theSearchParameter) {
		// combo unique search parameter
		if (hasAnyExtensionUniqueSetTo(theSearchParameter, true)) {
			return Set.of(STRING, TOKEN, DATE, QUANTITY, URI, NUMBER, REFERENCE);
			// combo non-unique search parameter or composite Search Parameter with HSearch indexing
		} else if (hasAnyExtensionUniqueSetTo(theSearchParameter, false)
				|| // combo non-unique search parameter
				myStorageSettings.isAdvancedHSearchIndexing()) { // composite Search Parameter with HSearch indexing
			return Set.of(STRING, TOKEN, DATE, QUANTITY, URI, NUMBER);
		} else { // composite Search Parameter (JPA only)
			return Set.of(STRING, TOKEN, DATE, QUANTITY);
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy