ca.uhn.fhir.mdm.rules.config.MdmRuleValidator Maven / Gradle / Ivy
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* 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.mdm.rules.config;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.mdm.api.IMdmRuleValidator;
import ca.uhn.fhir.mdm.api.MdmConstants;
import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
import ca.uhn.fhir.mdm.rules.json.MdmFilterSearchParamJson;
import ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.mdm.rules.json.MdmSimilarityJson;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.SearchParameterUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Service
public class MdmRuleValidator implements IMdmRuleValidator {
private static final Logger ourLog = LoggerFactory.getLogger(MdmRuleValidator.class);
private final FhirContext myFhirContext;
private final ISearchParamRegistry mySearchParamRetriever;
private final FhirTerser myTerser;
private final IFhirPath myFhirPath;
@Autowired
public MdmRuleValidator(FhirContext theFhirContext, ISearchParamRegistry theSearchParamRetriever) {
myFhirContext = theFhirContext;
myTerser = myFhirContext.newTerser();
if (myFhirContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
myFhirPath = myFhirContext.newFhirPath();
} else {
ourLog.debug("Skipping FHIRPath validation as DSTU2 does not support FHIR");
myFhirPath = null;
}
mySearchParamRetriever = theSearchParamRetriever;
}
public void validate(MdmRulesJson theMdmRules) {
validateMdmTypes(theMdmRules);
validateSearchParams(theMdmRules);
validateMatchFields(theMdmRules);
validateSystemsAreUris(theMdmRules);
validateEidSystemsMatchMdmTypes(theMdmRules);
}
private void validateEidSystemsMatchMdmTypes(MdmRulesJson theMdmRules) {
theMdmRules.getEnterpriseEIDSystems().keySet().forEach(key -> {
// Ensure each key is either * or a valid resource type.
if (!key.equalsIgnoreCase("*") && !theMdmRules.getMdmTypes().contains(key)) {
throw new ConfigurationException(Msg.code(1507)
+ String.format(
"There is an eidSystem set for [%s] but that is not one of the mdmTypes. Valid options are [%s].",
key, buildValidEidKeysMessage(theMdmRules)));
}
});
}
private String buildValidEidKeysMessage(MdmRulesJson theMdmRulesJson) {
List validTypes = new ArrayList<>(theMdmRulesJson.getMdmTypes());
validTypes.add("*");
return String.join(", ", validTypes);
}
private void validateSystemsAreUris(MdmRulesJson theMdmRules) {
theMdmRules.getEnterpriseEIDSystems().entrySet().forEach(entry -> {
String resourceType = entry.getKey();
String uri = entry.getValue();
if (!resourceType.equals("*")) {
try {
myFhirContext.getResourceType(resourceType);
} catch (DataFormatException e) {
throw new ConfigurationException(Msg.code(1508)
+ String.format(
"%s is not a valid resource type, but is set in the eidSystems field.",
resourceType));
}
}
validateIsUri(uri);
});
}
public void validateMdmTypes(MdmRulesJson theMdmRulesJson) {
ourLog.info("Validating MDM types {}", theMdmRulesJson.getMdmTypes());
if (theMdmRulesJson.getMdmTypes() == null) {
throw new ConfigurationException(Msg.code(1509) + "mdmTypes must be set to a list of resource types.");
}
for (String resourceType : theMdmRulesJson.getMdmTypes()) {
validateTypeHasIdentifier(resourceType);
}
}
public void validateTypeHasIdentifier(String theResourceType) {
if (mySearchParamRetriever.getActiveSearchParam(theResourceType, "identifier") == null) {
throw new ConfigurationException(
Msg.code(1510) + "Resource Type " + theResourceType
+ " is not supported, as it does not have an 'identifier' field, which is necessary for MDM workflow.");
}
}
private void validateSearchParams(MdmRulesJson theMdmRulesJson) {
ourLog.info("Validating search parameters {}", theMdmRulesJson.getCandidateSearchParams());
if (theMdmRulesJson.getCandidateSearchParams().isEmpty()) {
ourLog.warn("No candidate search parameter was found. Defining candidate search parameter is strongly "
+ "recommended for better performance of MDM");
}
for (MdmResourceSearchParamJson searchParams : theMdmRulesJson.getCandidateSearchParams()) {
searchParams
.iterator()
.forEachRemaining(searchParam ->
validateSearchParam("candidateSearchParams", searchParams.getResourceType(), searchParam));
}
for (MdmFilterSearchParamJson filter : theMdmRulesJson.getCandidateFilterSearchParams()) {
validateSearchParam("candidateFilterSearchParams", filter.getResourceType(), filter.getSearchParam());
}
}
private void validateSearchParam(String theFieldName, String theTheResourceType, String theTheSearchParam) {
if (MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(theTheResourceType)) {
validateResourceSearchParam(theFieldName, "Patient", theTheSearchParam);
validateResourceSearchParam(theFieldName, "Practitioner", theTheSearchParam);
} else {
validateResourceSearchParam(theFieldName, theTheResourceType, theTheSearchParam);
}
}
private void validateResourceSearchParam(String theFieldName, String theResourceType, String theSearchParam) {
String searchParam = SearchParameterUtil.stripModifier(theSearchParam);
if (mySearchParamRetriever.getActiveSearchParam(theResourceType, searchParam) == null) {
throw new ConfigurationException(Msg.code(1511) + "Error in " + theFieldName + ": " + theResourceType
+ " does not have a search parameter called '" + theSearchParam + "'");
}
}
private void validateMatchFields(MdmRulesJson theMdmRulesJson) {
ourLog.info("Validating match fields {}", theMdmRulesJson.getMatchFields());
Set names = new HashSet<>();
for (MdmFieldMatchJson fieldMatch : theMdmRulesJson.getMatchFields()) {
if (names.contains(fieldMatch.getName())) {
throw new ConfigurationException(
Msg.code(1512) + "Two MatchFields have the same name '" + fieldMatch.getName() + "'");
}
names.add(fieldMatch.getName());
if (fieldMatch.getSimilarity() != null) {
validateSimilarity(fieldMatch);
} else if (fieldMatch.getMatcher() == null) {
throw new ConfigurationException(Msg.code(1513) + "MatchField " + fieldMatch.getName()
+ " has neither a similarity nor a matcher. At least one must be present.");
}
validatePath(theMdmRulesJson.getMdmTypes(), fieldMatch);
}
}
private void validateSimilarity(MdmFieldMatchJson theFieldMatch) {
MdmSimilarityJson similarity = theFieldMatch.getSimilarity();
if (similarity.getMatchThreshold() == null) {
throw new ConfigurationException(Msg.code(1514) + "MatchField " + theFieldMatch.getName() + " similarity "
+ similarity.getAlgorithm() + " requires a matchThreshold");
}
}
private void validatePath(List theMdmTypes, MdmFieldMatchJson theFieldMatch) {
String resourceType = theFieldMatch.getResourceType();
if (MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(resourceType)) {
validateFieldPathForAllTypes(theMdmTypes, theFieldMatch);
} else {
validateFieldPath(theFieldMatch);
}
}
private void validateFieldPathForAllTypes(List theMdmResourceTypes, MdmFieldMatchJson theFieldMatch) {
for (String resourceType : theMdmResourceTypes) {
validateFieldPathForType(resourceType, theFieldMatch);
}
}
private void validateFieldPathForType(String theResourceType, MdmFieldMatchJson theFieldMatch) {
ourLog.debug("Validating resource {} for {} ", theResourceType, theFieldMatch.getResourcePath());
if (theFieldMatch.getFhirPath() != null && theFieldMatch.getResourcePath() != null) {
throw new ConfigurationException(Msg.code(1515) + "MatchField [" + theFieldMatch.getName()
+ "] resourceType ["
+ theFieldMatch.getResourceType()
+ "] has defined both a resourcePath and a fhirPath. You must define one of the two.");
}
if (theFieldMatch.getResourcePath() == null && theFieldMatch.getFhirPath() == null) {
throw new ConfigurationException(Msg.code(1516) + "MatchField [" + theFieldMatch.getName()
+ "] resourceType ["
+ theFieldMatch.getResourceType()
+ "] has defined neither a resourcePath or a fhirPath. You must define one of the two.");
}
if (theFieldMatch.getResourcePath() != null) {
try { // Try to validate the struture definition path
RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResourceType);
Class extends IBaseResource> implementingClass = resourceDefinition.getImplementingClass();
String path = theResourceType + "." + theFieldMatch.getResourcePath();
myTerser.getDefinition(implementingClass, path);
} catch (DataFormatException | ConfigurationException | ClassCastException e) {
// Fallback to attempting to FHIRPath evaluate it.
throw new ConfigurationException(Msg.code(1517) + "MatchField " + theFieldMatch.getName()
+ " resourceType "
+ theFieldMatch.getResourceType()
+ " has invalid path '"
+ theFieldMatch.getResourcePath() + "'. " + e.getMessage());
}
} else { // Try to validate the FHIRPath
try {
if (myFhirPath != null) {
myFhirPath.parse(theResourceType + "." + theFieldMatch.getFhirPath());
} else {
ourLog.debug("Can't validate FHIRPath expression due to a lack of IFhirPath object.");
}
} catch (Exception e) {
throw new ConfigurationException(Msg.code(1518) + "MatchField [" + theFieldMatch.getName()
+ "] resourceType [" + theFieldMatch.getResourceType() + "] has failed FHIRPath evaluation. "
+ e.getMessage());
}
}
}
private void validateFieldPath(MdmFieldMatchJson theFieldMatch) {
validateFieldPathForType(theFieldMatch.getResourceType(), theFieldMatch);
}
private void validateIsUri(String theUri) {
ourLog.info("Validating system URI {}", theUri);
try {
new URI(theUri);
} catch (URISyntaxException e) {
throw new ConfigurationException(
Msg.code(1519) + "Enterprise Identifier System (eidSystem) must be a valid URI");
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy