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}