001package ca.uhn.fhir.rest.server.interceptor.s13n; 002 003/*- 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.fhirpath.FhirPathExecutionException; 026import ca.uhn.fhir.fhirpath.IFhirPath; 027import ca.uhn.fhir.interceptor.api.Hook; 028import ca.uhn.fhir.interceptor.api.Interceptor; 029import ca.uhn.fhir.interceptor.api.Pointcut; 030import ca.uhn.fhir.rest.api.server.RequestDetails; 031import ca.uhn.fhir.rest.server.interceptor.ConfigLoader; 032import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.EmailStandardizer; 033import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.FirstNameStandardizer; 034import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.IStandardizer; 035import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.LastNameStandardizer; 036import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.PhoneStandardizer; 037import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.TextStandardizer; 038import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.TitleStandardizer; 039import org.hl7.fhir.instance.model.api.IBase; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041import org.hl7.fhir.instance.model.api.IPrimitiveType; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044 045import java.util.HashMap; 046import java.util.List; 047import java.util.Map; 048 049@Interceptor 050public class StandardizingInterceptor { 051 052 /** 053 * Pre-defined standardizers 054 */ 055 public enum StandardizationType { 056 NAME_FAMILY, NAME_GIVEN, EMAIL, TITLE, PHONE, TEXT; 057 } 058 059 public static final String STANDARDIZATION_DISABLED_HEADER = "HAPI-Standardization-Disabled"; 060 061 private static final Logger ourLog = LoggerFactory.getLogger(StandardizingInterceptor.class); 062 063 private Map<String, Map<String, String>> myConfig; 064 private Map<String, IStandardizer> myStandardizers = new HashMap<>(); 065 066 public StandardizingInterceptor() { 067 super(); 068 069 ourLog.info("Starting StandardizingInterceptor {}", this); 070 071 myConfig = ConfigLoader.loadJson("classpath:field-s13n-rules.json", Map.class); 072 initStandardizers(); 073 } 074 075 public StandardizingInterceptor(Map<String, Map<String, String>> theConfig) { 076 super(); 077 myConfig = theConfig; 078 initStandardizers(); 079 } 080 081 public void initStandardizers() { 082 myStandardizers.put(StandardizationType.NAME_FAMILY.name(), new LastNameStandardizer()); 083 myStandardizers.put(StandardizationType.NAME_GIVEN.name(), new FirstNameStandardizer()); 084 myStandardizers.put(StandardizationType.EMAIL.name(), new EmailStandardizer()); 085 myStandardizers.put(StandardizationType.TITLE.name(), new TitleStandardizer()); 086 myStandardizers.put(StandardizationType.PHONE.name(), new PhoneStandardizer()); 087 myStandardizers.put(StandardizationType.TEXT.name(), new TextStandardizer()); 088 089 ourLog.info("Initialized standardizers {}", myStandardizers); 090 } 091 092 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) 093 public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) { 094 ourLog.debug("Standardizing on pre-create for - {}, {}", theRequest, theResource); 095 standardize(theRequest, theResource); 096 } 097 098 @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED) 099 public void resourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { 100 ourLog.debug("Standardizing on pre-update for - {}, {}, {}", theRequest, theOldResource, theNewResource); 101 standardize(theRequest, theNewResource); 102 } 103 104 private void standardize(RequestDetails theRequest, IBaseResource theResource) { 105 if (theRequest == null) { 106 ourLog.debug("RequestDetails is null - unable to standardize {}", theResource); 107 return; 108 } 109 110 if (!theRequest.getHeaders(STANDARDIZATION_DISABLED_HEADER).isEmpty()) { 111 ourLog.debug("Standardization for {} is disabled via header {}", theResource, STANDARDIZATION_DISABLED_HEADER); 112 return; 113 } 114 115 if (theResource == null) { 116 ourLog.debug("Nothing to standardize for {}", theRequest); 117 return; 118 } 119 120 FhirContext ctx = theRequest.getFhirContext(); 121 122 String resourceType = ctx.getResourceType(theResource); 123 IFhirPath fhirPath = ctx.newFhirPath(); 124 125 for (Map.Entry<String, Map<String, String>> rule : myConfig.entrySet()) { 126 String resourceFromConfig = rule.getKey(); 127 if (!appliesToResource(resourceFromConfig, resourceType)) { 128 continue; 129 } 130 131 standardize(theResource, rule.getValue(), fhirPath); 132 } 133 } 134 135 private void standardize(IBaseResource theResource, Map<String, String> theRules, IFhirPath theFhirPath) { 136 for (Map.Entry<String, String> rule : theRules.entrySet()) { 137 IStandardizer std = getStandardizer(rule); 138 List<IBase> values; 139 try { 140 values = theFhirPath.evaluate(theResource, rule.getKey(), IBase.class); 141 } catch (FhirPathExecutionException e) { 142 ourLog.warn("Unable to evaluate path at {} for {}", rule.getKey(), theResource); 143 return; 144 } 145 146 for (IBase v : values) { 147 if (!(v instanceof IPrimitiveType)) { 148 ourLog.warn("Value at path {} is of type {}, which is not of primitive type - skipping", rule.getKey(), v.fhirType()); 149 continue; 150 } 151 IPrimitiveType<?> value = (IPrimitiveType<?>) v; 152 String valueString = value.getValueAsString(); 153 String standardizedValueString = std.standardize(valueString); 154 value.setValueAsString(standardizedValueString); 155 ourLog.debug("Standardized {} to {}", valueString, standardizedValueString); 156 } 157 } 158 } 159 160 private IStandardizer getStandardizer(Map.Entry<String, String> rule) { 161 String standardizerName = rule.getValue(); 162 if (myStandardizers.containsKey(standardizerName)) { 163 return myStandardizers.get(standardizerName); 164 } 165 166 IStandardizer standardizer; 167 try { 168 standardizer = (IStandardizer) Class.forName(standardizerName).getDeclaredConstructor().newInstance(); 169 } catch (Exception e) { 170 throw new RuntimeException(Msg.code(349) + String.format("Unable to create standardizer %s", standardizerName), e); 171 } 172 173 myStandardizers.put(standardizerName, standardizer); 174 return standardizer; 175 } 176 177 private boolean appliesToResource(String theResourceFromConfig, String theActualResourceType) { 178 return theResourceFromConfig.equals(theActualResourceType); 179 } 180 181 public Map<String, Map<String, String>> getConfig() { 182 return myConfig; 183 } 184 185 public void setConfig(Map<String, Map<String, String>> theConfig) { 186 myConfig = theConfig; 187 } 188 189 public Map<String, IStandardizer> getStandardizers() { 190 return myStandardizers; 191 } 192 193 public void setStandardizers(Map<String, IStandardizer> theStandardizers) { 194 myStandardizers = theStandardizers; 195 } 196}