001/*- 002 * #%L 003 * HAPI FHIR JPA Server - Master Data Management 004 * %% 005 * Copyright (C) 2014 - 2023 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.jpa.mdm.svc; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.fhirpath.FhirPathExecutionException; 024import ca.uhn.fhir.fhirpath.IFhirPath; 025import ca.uhn.fhir.mdm.blocklist.json.BlockListJson; 026import ca.uhn.fhir.mdm.blocklist.json.BlockListRuleJson; 027import ca.uhn.fhir.mdm.blocklist.json.BlockedFieldJson; 028import ca.uhn.fhir.mdm.blocklist.svc.IBlockListRuleProvider; 029import ca.uhn.fhir.mdm.blocklist.svc.IBlockRuleEvaluationSvc; 030import ca.uhn.fhir.util.FhirTypeUtil; 031import org.hl7.fhir.instance.model.api.IAnyResource; 032import org.hl7.fhir.instance.model.api.IBase; 033import org.hl7.fhir.instance.model.api.IPrimitiveType; 034import org.slf4j.Logger; 035 036import java.util.List; 037import javax.annotation.Nullable; 038 039import static org.slf4j.LoggerFactory.getLogger; 040 041/** 042 * An implementation of IBlockRuleEvaluationSvc. 043 * Evaluates whether or not a provided resource 044 * is blocked from mdm matching or not. 045 */ 046public class BlockRuleEvaluationSvcImpl implements IBlockRuleEvaluationSvc { 047 private static final Logger ourLog = getLogger(BlockRuleEvaluationSvcImpl.class); 048 049 private final IFhirPath myFhirPath; 050 051 private final IBlockListRuleProvider myBlockListRuleProvider; 052 053 public BlockRuleEvaluationSvcImpl( 054 FhirContext theContext, @Nullable IBlockListRuleProvider theIBlockListRuleProvider) { 055 myFhirPath = theContext.newFhirPath(); 056 myBlockListRuleProvider = theIBlockListRuleProvider; 057 } 058 059 private boolean hasBlockList() { 060 return myBlockListRuleProvider != null && myBlockListRuleProvider.getBlocklistRules() != null; 061 } 062 063 @Override 064 public boolean isMdmMatchingBlocked(IAnyResource theResource) { 065 if (hasBlockList()) { 066 return isMdmMatchingBlockedInternal(theResource); 067 } 068 return false; 069 } 070 071 private boolean isMdmMatchingBlockedInternal(IAnyResource theResource) { 072 BlockListJson blockListJson = myBlockListRuleProvider.getBlocklistRules(); 073 String resourceType = theResource.fhirType(); 074 075 // gather only applicable rules 076 // these rules are 'or''d, so if any match, 077 // mdm matching is blocked 078 return blockListJson.getBlockListItemJsonList().stream() 079 .filter(r -> r.getResourceType().equals(resourceType)) 080 .anyMatch(rule -> isMdmBlockedForFhirPath(theResource, rule)); 081 } 082 083 private boolean isMdmBlockedForFhirPath(IAnyResource theResource, BlockListRuleJson theRule) { 084 List<BlockedFieldJson> blockedFields = theRule.getBlockedFields(); 085 086 // rules are 'and'ed 087 // This means that if we detect any reason *not* to block 088 // we don't; only if all block rules pass do we block 089 for (BlockedFieldJson field : blockedFields) { 090 String path = field.getFhirPath(); 091 String blockedValue = field.getBlockedValue(); 092 093 List<IBase> results; 094 try { 095 // can throw FhirPathExecutionException if path is incorrect 096 // or functions are invalid. 097 // single() explicitly throws this (but may be what is desired) 098 // so we'll catch and not block if this fails 099 results = myFhirPath.evaluate(theResource, path, IBase.class); 100 } catch (FhirPathExecutionException ex) { 101 ourLog.warn( 102 "FhirPath evaluation failed with an exception." 103 + " No blocking will be applied and mdm matching will continue as before.", 104 ex); 105 return false; 106 } 107 108 // fhir path should return exact values 109 if (results.size() != 1) { 110 // no results means no blocking 111 // too many matches means no blocking 112 ourLog.trace("Too many values at field {}", path); 113 return false; 114 } 115 116 IBase first = results.get(0); 117 118 if (FhirTypeUtil.isPrimitiveType(first.fhirType())) { 119 IPrimitiveType<?> primitiveType = (IPrimitiveType<?>) first; 120 if (!primitiveType.getValueAsString().equalsIgnoreCase(blockedValue)) { 121 // doesn't match 122 // no block 123 ourLog.trace("Value at path {} does not match - mdm will not block.", path); 124 return false; 125 } 126 } else { 127 // blocking can only be done by evaluating primitive types 128 // additional fhirpath values required 129 ourLog.warn( 130 "FhirPath {} yields a non-primitive value; blocking is only supported on primitive field types.", 131 path); 132 return false; 133 } 134 } 135 136 // if we got here, all blocking rules evaluated to true 137 return true; 138 } 139}