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