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}