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