Merge pull request #2459 from hapifhir/ng_validation_s13n_interceptors
Interceptors to handle standardization, normalization and address and field-level validation.
This commit is contained in:
commit
c7e8342c29
|
@ -0,0 +1,178 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Core Library
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseDatatype;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseExtension;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for modifying with extensions in a FHIR version-independent approach.
|
||||||
|
*/
|
||||||
|
public class ExtensionUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an extension with the specified URL creating one if it doesn't exist.
|
||||||
|
*
|
||||||
|
* @param theBase Base resource to get extension from
|
||||||
|
* @param theUrl URL for the extension
|
||||||
|
* @return Returns a extension with the specified URL.
|
||||||
|
* @throws IllegalArgumentException IllegalArgumentException is thrown in case resource doesn't support extensions
|
||||||
|
*/
|
||||||
|
public static IBaseExtension<?, ?> getOrCreateExtension(IBase theBase, String theUrl) {
|
||||||
|
IBaseHasExtensions baseHasExtensions = validateExtensionSupport(theBase);
|
||||||
|
IBaseExtension extension = getExtension(baseHasExtensions, theUrl);
|
||||||
|
if (extension == null) {
|
||||||
|
extension = baseHasExtensions.addExtension();
|
||||||
|
extension.setUrl(theUrl);
|
||||||
|
}
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IBaseHasExtensions validateExtensionSupport(IBase theBase) {
|
||||||
|
if (!(theBase instanceof IBaseHasExtensions)) {
|
||||||
|
throw new IllegalArgumentException(String.format("Expected instance that supports extensions, but got %s", theBase));
|
||||||
|
}
|
||||||
|
return (IBaseHasExtensions) theBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified instance has an extension with the specified URL
|
||||||
|
*
|
||||||
|
* @param theBase The base resource to check extensions on
|
||||||
|
* @param theExtensionUrl URL of the extension
|
||||||
|
* @return Returns true if extension is exists and false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean hasExtension(IBase theBase, String theExtensionUrl) {
|
||||||
|
IBaseHasExtensions baseHasExtensions;
|
||||||
|
try {
|
||||||
|
baseHasExtensions = validateExtensionSupport(theBase);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getExtension(baseHasExtensions, theExtensionUrl) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified instance has an extension with the specified URL
|
||||||
|
*
|
||||||
|
* @param theBase The base resource to check extensions on
|
||||||
|
* @param theExtensionUrl URL of the extension
|
||||||
|
* @return Returns true if extension is exists and false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean hasExtension(IBase theBase, String theExtensionUrl, String theExtensionValue) {
|
||||||
|
if (!hasExtension(theBase, theExtensionUrl)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IBaseDatatype value = getExtension((IBaseHasExtensions) theBase, theExtensionUrl).getValue();
|
||||||
|
if (value == null) {
|
||||||
|
return theExtensionValue == null;
|
||||||
|
}
|
||||||
|
return value.toString().equals(theExtensionValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the first extension with the specified URL
|
||||||
|
*
|
||||||
|
* @param theBase The resource to get the extension for
|
||||||
|
* @param theExtensionUrl URL of the extension to get. Must be non-null
|
||||||
|
* @return Returns the first available extension with the specified URL, or null if such extension doesn't exist
|
||||||
|
*/
|
||||||
|
public static IBaseExtension<?, ?> getExtension(IBaseHasExtensions theBase, String theExtensionUrl) {
|
||||||
|
return theBase.getExtension()
|
||||||
|
.stream()
|
||||||
|
.filter(e -> theExtensionUrl.equals(e.getUrl()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all extensions with the specified URL
|
||||||
|
*
|
||||||
|
* @param theBase The resource to get the extension for
|
||||||
|
* @param theExtensionUrl URL of the extension to get. Must be non-null
|
||||||
|
* @return Returns all extension with the specified URL, or an empty list if such extensions do not exist
|
||||||
|
*/
|
||||||
|
public static List<IBaseExtension<?, ?>> getExtensions(IBaseHasExtensions theBase, String theExtensionUrl) {
|
||||||
|
return theBase.getExtension()
|
||||||
|
.stream()
|
||||||
|
.filter(e -> theExtensionUrl.equals(e.getUrl()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets value of the extension as a string
|
||||||
|
*
|
||||||
|
* @param theExtension The extension to set the value on
|
||||||
|
* @param theValue The value to set
|
||||||
|
* @param theFhirContext The context containing FHIR resource definitions
|
||||||
|
*/
|
||||||
|
public static void setExtension(FhirContext theFhirContext, IBaseExtension theExtension, String theValue) {
|
||||||
|
setExtension(theFhirContext, theExtension, "string", theValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets value of the extension
|
||||||
|
*
|
||||||
|
* @param theExtension The extension to set the value on
|
||||||
|
* @param theExtensionType Element type of the extension
|
||||||
|
* @param theValue The value to set
|
||||||
|
* @param theFhirContext The context containing FHIR resource definitions
|
||||||
|
*/
|
||||||
|
public static void setExtension(FhirContext theFhirContext, IBaseExtension theExtension, String theExtensionType, String theValue) {
|
||||||
|
theExtension.setValue(TerserUtil.newElement(theFhirContext, theExtensionType, theValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets or replaces existing extension with the specified value as a string
|
||||||
|
*
|
||||||
|
* @param theBase The resource to update extension on
|
||||||
|
* @param theUrl Extension URL
|
||||||
|
* @param theValue Extension value
|
||||||
|
* @param theFhirContext The context containing FHIR resource definitions
|
||||||
|
*/
|
||||||
|
public static void setExtension(FhirContext theFhirContext, IBase theBase, String theUrl, String theValue) {
|
||||||
|
IBaseExtension ext = getOrCreateExtension(theBase, theUrl);
|
||||||
|
setExtension(theFhirContext, ext, theValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets or replaces existing extension with the specified value
|
||||||
|
*
|
||||||
|
* @param theBase The resource to update extension on
|
||||||
|
* @param theUrl Extension URL
|
||||||
|
* @param theValueType Type of the value to set in the extension
|
||||||
|
* @param theValue Extension value
|
||||||
|
* @param theFhirContext The context containing FHIR resource definitions
|
||||||
|
*/
|
||||||
|
public static void setExtension(FhirContext theFhirContext, IBase theBase, String theUrl, String theValueType, String theValue) {
|
||||||
|
IBaseExtension ext = getOrCreateExtension(theBase, theUrl);
|
||||||
|
setExtension(theFhirContext, ext, theValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Core Library
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean-value function for comparing two FHIR primitives via <code>.equals()</code> method on the instance
|
||||||
|
* internal values.
|
||||||
|
*/
|
||||||
|
public class PrimitiveTypeEqualsPredicate implements BiPredicate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if both bases are of the same type and hold the same values.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean test(Object theBase1, Object theBase2) {
|
||||||
|
if (theBase1 == null) {
|
||||||
|
return theBase2 == null;
|
||||||
|
}
|
||||||
|
if (theBase2 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!theBase1.getClass().equals(theBase2.getClass())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Field f : theBase1.getClass().getDeclaredFields()) {
|
||||||
|
Class<?> fieldClass = f.getType();
|
||||||
|
|
||||||
|
if (!IPrimitiveType.class.isAssignableFrom(fieldClass)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPrimitiveType<?> val1, val2;
|
||||||
|
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
val1 = (IPrimitiveType<?>) f.get(theBase1);
|
||||||
|
val2 = (IPrimitiveType<?>) f.get(theBase2);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// swallow
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val1 == null && val2 == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val1 == null || val2 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object actualVal1 = val1.getValue();
|
||||||
|
Object actualVal2 = val2.getValue();
|
||||||
|
|
||||||
|
if (actualVal1 == null && actualVal2 == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (actualVal1 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!actualVal1.equals(actualVal2)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Core Library
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for handling updates of the instances that support property modification via <code>setProperty</code>
|
||||||
|
* and <code>getProperty</code> methods.
|
||||||
|
*/
|
||||||
|
public class PropertyModifyingHelper {
|
||||||
|
|
||||||
|
public static final String GET_PROPERTY_METHOD_NAME = "getProperty";
|
||||||
|
public static final String SET_PROPERTY_METHOD_NAME = "setProperty";
|
||||||
|
public static final String DEFAULT_DELIMITER = ", ";
|
||||||
|
|
||||||
|
private IBase myBase;
|
||||||
|
|
||||||
|
private String myDelimiter = DEFAULT_DELIMITER;
|
||||||
|
|
||||||
|
private FhirContext myFhirContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance initializing the dependencies.
|
||||||
|
*
|
||||||
|
* @param theFhirContext FHIR context holding the resource definitions
|
||||||
|
* @param theBase The base class to set properties on
|
||||||
|
*/
|
||||||
|
public PropertyModifyingHelper(FhirContext theFhirContext, IBase theBase) {
|
||||||
|
if (findGetPropertyMethod(theBase) == null) {
|
||||||
|
throw new IllegalArgumentException("Specified base instance does not support property retrieval.");
|
||||||
|
}
|
||||||
|
myBase = theBase;
|
||||||
|
myFhirContext = theFhirContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the method with the specified name and parameter types.
|
||||||
|
*
|
||||||
|
* @param theObject Non-null instance to get the method from
|
||||||
|
* @param theMethodName Name of the method to get
|
||||||
|
* @param theParamClasses Parameters types that method parameters should be assignable as
|
||||||
|
* @return Returns the method with the given name and parameters or null if it can't be found
|
||||||
|
*/
|
||||||
|
protected Method getMethod(Object theObject, String theMethodName, Class... theParamClasses) {
|
||||||
|
for (Method m : theObject.getClass().getDeclaredMethods()) {
|
||||||
|
if (m.getName().equals(theMethodName)) {
|
||||||
|
if (theParamClasses.length == 0) {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
if (m.getParameterCount() != theParamClasses.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < theParamClasses.length; i++) {
|
||||||
|
if (!m.getParameterTypes()[i].isAssignableFrom(theParamClasses[i])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all non-blank fields as a single string joined with the delimiter provided by {@link #getDelimiter()}
|
||||||
|
*
|
||||||
|
* @param theFiledNames Field names to retrieve values for
|
||||||
|
* @return Returns all specified non-blank fileds as a single string.
|
||||||
|
*/
|
||||||
|
public String getFields(String... theFiledNames) {
|
||||||
|
return Arrays.stream(theFiledNames)
|
||||||
|
.map(this::get)
|
||||||
|
.filter(s -> !StringUtils.isBlank(s))
|
||||||
|
.collect(Collectors.joining(getDelimiter()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets property with the specified name from the provided base class.
|
||||||
|
*
|
||||||
|
* @param thePropertyName Name of the property to get
|
||||||
|
* @return Returns property value converted to string. In case of multiple values, they are joined with the
|
||||||
|
* specified delimiter.
|
||||||
|
*/
|
||||||
|
public String get(String thePropertyName) {
|
||||||
|
return getMultiple(thePropertyName)
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.joining(getDelimiter()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets property or adds to a collection of properties with the specified name from the provided base class.
|
||||||
|
*
|
||||||
|
* @param thePropertyName Name of the property to set or add element to in case property is a collection
|
||||||
|
*/
|
||||||
|
public void set(String thePropertyName, String theValue) {
|
||||||
|
if (theValue == null || theValue.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
IBase value = myFhirContext.getElementDefinition("string").newInstance(theValue);
|
||||||
|
Method setPropertyMethod = findSetPropertyMethod(myBase, int.class, String.class, value.getClass());
|
||||||
|
int hashCode = thePropertyName.hashCode();
|
||||||
|
setPropertyMethod.invoke(myBase, hashCode, thePropertyName, value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(String.format("Unable to set property %s on %s", thePropertyName, myBase), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets property values with the specified name from the provided base class.
|
||||||
|
*
|
||||||
|
* @param thePropertyName Name of the property to get
|
||||||
|
* @return Returns property values converted to string.
|
||||||
|
*/
|
||||||
|
public List<String> getMultiple(String thePropertyName) {
|
||||||
|
Method getPropertyMethod = findGetPropertyMethod(myBase);
|
||||||
|
Object[] values;
|
||||||
|
try {
|
||||||
|
values = (Object[]) getPropertyMethod.invoke(myBase, thePropertyName.hashCode(), thePropertyName, true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(String.format("Instance %s does not supply property %s", myBase, thePropertyName), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.stream(values)
|
||||||
|
.map(String::valueOf)
|
||||||
|
.filter(s -> !StringUtils.isEmpty(s))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method findGetPropertyMethod(IBase theAddress) {
|
||||||
|
return getMethod(theAddress, GET_PROPERTY_METHOD_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method findSetPropertyMethod(IBase theAddress, Class... theParamClasses) {
|
||||||
|
return getMethod(theAddress, SET_PROPERTY_METHOD_NAME, theParamClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the delimiter used when concatenating multiple field values
|
||||||
|
*
|
||||||
|
* @return Returns the delimiter
|
||||||
|
*/
|
||||||
|
public String getDelimiter() {
|
||||||
|
return myDelimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the delimiter used when concatenating multiple field values
|
||||||
|
*
|
||||||
|
* @param theDelimiter The delimiter to set
|
||||||
|
*/
|
||||||
|
public void setDelimiter(String theDelimiter) {
|
||||||
|
this.myDelimiter = theDelimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the base instance that this helper operates on
|
||||||
|
*
|
||||||
|
* @return Returns the base instance
|
||||||
|
*/
|
||||||
|
public IBase getBase() {
|
||||||
|
return myBase;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,481 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Core Library
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||||
|
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
|
||||||
|
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.slf4j.LoggerFactory.getLogger;
|
||||||
|
|
||||||
|
public final class TerserUtil {
|
||||||
|
|
||||||
|
private static final Logger ourLog = getLogger(TerserUtil.class);
|
||||||
|
|
||||||
|
public static final String FIELD_NAME_IDENTIFIER = "identifier";
|
||||||
|
|
||||||
|
public static final Collection<String> IDS_AND_META_EXCLUDES =
|
||||||
|
Collections.unmodifiableSet(Stream.of("id", "identifier", "meta").collect(Collectors.toSet()));
|
||||||
|
|
||||||
|
public static final Predicate<String> EXCLUDE_IDS_AND_META = new Predicate<String>() {
|
||||||
|
@Override
|
||||||
|
public boolean test(String s) {
|
||||||
|
return !IDS_AND_META_EXCLUDES.contains(s);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Predicate<String> INCLUDE_ALL = new Predicate<String>() {
|
||||||
|
@Override
|
||||||
|
public boolean test(String s) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private TerserUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an Child Definition of `identifier`, a R4/DSTU3 EID Identifier, and a new resource, clone the EID into that resources' identifier list.
|
||||||
|
*/
|
||||||
|
public static void cloneEidIntoResource(FhirContext theFhirContext, BaseRuntimeChildDefinition theIdentifierDefinition, IBase theEid, IBase theResourceToCloneEidInto) {
|
||||||
|
// FHIR choice types - fields within fhir where we have a choice of ids
|
||||||
|
BaseRuntimeElementCompositeDefinition<?> childIdentifier = (BaseRuntimeElementCompositeDefinition<?>) theIdentifierDefinition.getChildByName(FIELD_NAME_IDENTIFIER);
|
||||||
|
IBase resourceNewIdentifier = childIdentifier.newInstance();
|
||||||
|
|
||||||
|
FhirTerser terser = theFhirContext.newTerser();
|
||||||
|
terser.cloneInto(theEid, resourceNewIdentifier, true);
|
||||||
|
theIdentifierDefinition.getMutator().addValue(theResourceToCloneEidInto, resourceNewIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified fields has any values
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theResource Resource to check if the specified field is set
|
||||||
|
* @param theFieldName name of the field to check
|
||||||
|
* @return Returns true if field exists and has any values set, and false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean hasValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) {
|
||||||
|
RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResource);
|
||||||
|
BaseRuntimeChildDefinition resourceIdentifier = resourceDefinition.getChildByName(theFieldName);
|
||||||
|
if (resourceIdentifier == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !(resourceIdentifier.getAccessor().getValues(theResource).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the Values of a specified field.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theResource Resource to check if the specified field is set
|
||||||
|
* @param theFieldName name of the field to check
|
||||||
|
* @return Returns true if field exists and has any values set, and false otherwise
|
||||||
|
*/
|
||||||
|
public static List<IBase> getValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) {
|
||||||
|
RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResource);
|
||||||
|
BaseRuntimeChildDefinition resourceIdentifier = resourceDefinition.getChildByName(theFieldName);
|
||||||
|
if (resourceIdentifier == null) {
|
||||||
|
ourLog.info("There is no field named {} in Resource {}", theFieldName, resourceDefinition.getName());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resourceIdentifier.getAccessor().getValues(theResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones specified composite field (collection). Composite field values must conform to the collections
|
||||||
|
* contract.
|
||||||
|
*
|
||||||
|
* @param theFrom Resource to clone the specified field from
|
||||||
|
* @param theTo Resource to clone the specified field to
|
||||||
|
* @param theField Field name to be copied
|
||||||
|
*/
|
||||||
|
public static void cloneCompositeField(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, String theField) {
|
||||||
|
FhirTerser terser = theFhirContext.newTerser();
|
||||||
|
|
||||||
|
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom);
|
||||||
|
BaseRuntimeChildDefinition childDefinition = definition.getChildByName(theField);
|
||||||
|
if (childDefinition == null) {
|
||||||
|
throw new IllegalArgumentException(String.format("Unable to find child definition %s in %s", theField, theFrom));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theFrom);
|
||||||
|
List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTo);
|
||||||
|
|
||||||
|
for (IBase theFromFieldValue : theFromFieldValues) {
|
||||||
|
if (containsPrimitiveValue(theFromFieldValue, theToFieldValues)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IBase newFieldValue = childDefinition.getChildByName(theField).newInstance();
|
||||||
|
terser.cloneInto(theFromFieldValue, newFieldValue, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
theToFieldValues.add(newFieldValue);
|
||||||
|
} catch (Exception e) {
|
||||||
|
childDefinition.getMutator().setValue(theTo, newFieldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsPrimitiveValue(IBase theItem, List<IBase> theItems) {
|
||||||
|
PrimitiveTypeEqualsPredicate predicate = new PrimitiveTypeEqualsPredicate();
|
||||||
|
return theItems.stream().anyMatch(i -> {
|
||||||
|
return predicate.test(i, theItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean contains(IBase theItem, List<IBase> theItems) {
|
||||||
|
Method method = null;
|
||||||
|
for (Method m : theItem.getClass().getDeclaredMethods()) {
|
||||||
|
if (m.getName().equals("equalsDeep")) {
|
||||||
|
method = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Method m = method;
|
||||||
|
return theItems.stream().anyMatch(i -> {
|
||||||
|
if (m != null) {
|
||||||
|
try {
|
||||||
|
return (Boolean) m.invoke(theItem, i);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Unable to compare equality via equalsDeep", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return theItem.equals(i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges all fields on the provided instance. <code>theTo</code> will contain a union of all values from <code>theFrom</code>
|
||||||
|
* instance and <code>theTo</code> instance.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theFrom The resource to merge the fields from
|
||||||
|
* @param theTo The resource to merge the fields into
|
||||||
|
*/
|
||||||
|
public static void mergeAllFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
|
mergeFields(theFhirContext, theFrom, theTo, INCLUDE_ALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all fields that test positive by the given inclusion strategy. <code>theTo</code> will contain a copy of the
|
||||||
|
* values from <code>theFrom</code> instance.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theFrom The resource to merge the fields from
|
||||||
|
* @param theTo The resource to merge the fields into
|
||||||
|
* @param inclusionStrategy Inclusion strategy that checks if a given field should be replaced by checking {@link Predicate#test(Object)}
|
||||||
|
*/
|
||||||
|
public static void replaceFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate<String> inclusionStrategy) {
|
||||||
|
FhirTerser terser = theFhirContext.newTerser();
|
||||||
|
|
||||||
|
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom);
|
||||||
|
for (BaseRuntimeChildDefinition childDefinition : definition.getChildrenAndExtension()) {
|
||||||
|
if (!inclusionStrategy.test(childDefinition.getElementName())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceField(theFrom, theTo, childDefinition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the field exists on the resource
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theFieldName Name of the field to check
|
||||||
|
* @param theInstance Resource instance to check
|
||||||
|
* @return Returns true if resource definition has a child with the specified name and false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean fieldExists(FhirContext theFhirContext, String theFieldName, IBaseResource theInstance) {
|
||||||
|
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theInstance);
|
||||||
|
return definition.getChildByName(theFieldName) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the specified fields on <code>theTo</code> resource with the value from <code>theFrom</code> resource.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theFrom The resource to replace the field from
|
||||||
|
* @param theTo The resource to replace the field on
|
||||||
|
*/
|
||||||
|
public static void replaceField(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
|
replaceField(theFhirContext, theFhirContext.newTerser(), theFieldName, theFrom, theTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link #replaceField(FhirContext, String, IBaseResource, IBaseResource)} instead
|
||||||
|
*/
|
||||||
|
public static void replaceField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
|
replaceField(theFrom, theTo, getBaseRuntimeChildDefinition(theFhirContext, theFieldName, theFrom));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the specified field on the resource provided
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theFieldName
|
||||||
|
* @param theResource
|
||||||
|
*/
|
||||||
|
public static void clearField(FhirContext theFhirContext, String theFieldName, IBaseResource theResource) {
|
||||||
|
BaseRuntimeChildDefinition childDefinition = getBaseRuntimeChildDefinition(theFhirContext, theFieldName, theResource);
|
||||||
|
childDefinition.getAccessor().getValues(theResource).clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the provided field with the given values. This method will add to the collection of existing field values
|
||||||
|
* in case of multiple cardinality. Use {@link #clearField(FhirContext, FhirTerser, String, IBaseResource, IBase...)}
|
||||||
|
* to remove values before setting
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theTerser Terser to be used when cloning field values
|
||||||
|
* @param theFieldName Child field name of the resource to set
|
||||||
|
* @param theResource The resource to set the values on
|
||||||
|
* @param theValues The values to set on the resource child field name
|
||||||
|
*/
|
||||||
|
public static void setField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theResource, IBase... theValues) {
|
||||||
|
BaseRuntimeChildDefinition childDefinition = getBaseRuntimeChildDefinition(theFhirContext, theFieldName, theResource);
|
||||||
|
|
||||||
|
List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theResource);
|
||||||
|
List<IBase> theToFieldValues = Arrays.asList(theValues);
|
||||||
|
|
||||||
|
mergeFields(theTerser, theResource, childDefinition, theFromFieldValues, theToFieldValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the specified value at the FHIR path provided.
|
||||||
|
*
|
||||||
|
* @param theTerser The terser that should be used for cloning the field value.
|
||||||
|
* @param theFhirPath The FHIR path to set the field at
|
||||||
|
* @param theResource The resource on which the value should be set
|
||||||
|
* @param theValue The value to set
|
||||||
|
*/
|
||||||
|
public static void setFieldByFhirPath(FhirTerser theTerser, String theFhirPath, IBaseResource theResource, IBase theValue) {
|
||||||
|
List<IBase> theFromFieldValues = theTerser.getValues(theResource, theFhirPath, true, false);
|
||||||
|
for (IBase theFromFieldValue : theFromFieldValues) {
|
||||||
|
theTerser.cloneInto(theValue, theFromFieldValue, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the specified value at the FHIR path provided.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theFhirPath The FHIR path to set the field at
|
||||||
|
* @param theResource The resource on which the value should be set
|
||||||
|
* @param theValue The value to set
|
||||||
|
*/
|
||||||
|
public static void setFieldByFhirPath(FhirContext theFhirContext, String theFhirPath, IBaseResource theResource, IBase theValue) {
|
||||||
|
setFieldByFhirPath(theFhirContext.newTerser(), theFhirPath, theResource, theValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void replaceField(IBaseResource theFrom, IBaseResource theTo, BaseRuntimeChildDefinition childDefinition) {
|
||||||
|
childDefinition.getAccessor().getFirstValueOrNull(theFrom).ifPresent(v -> {
|
||||||
|
childDefinition.getMutator().setValue(theTo, v);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges values of all fields except for "identifier" and "meta" from <code>theFrom</code> resource to
|
||||||
|
* <code>theTo</code> resource. Fields values are compared via the equalsDeep method, or via object identity if this
|
||||||
|
* method is not available.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theFrom Resource to merge the specified field from
|
||||||
|
* @param theTo Resource to merge the specified field into
|
||||||
|
*/
|
||||||
|
public static void mergeFieldsExceptIdAndMeta(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
|
mergeFields(theFhirContext, theFrom, theTo, EXCLUDE_IDS_AND_META);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges values of all field from <code>theFrom</code> resource to <code>theTo</code> resource. Fields
|
||||||
|
* values are compared via the equalsDeep method, or via object identity if this method is not available.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theFrom Resource to merge the specified field from
|
||||||
|
* @param theTo Resource to merge the specified field into
|
||||||
|
* @param inclusionStrategy Predicate to test which fields should be merged
|
||||||
|
*/
|
||||||
|
public static void mergeFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate<String> inclusionStrategy) {
|
||||||
|
FhirTerser terser = theFhirContext.newTerser();
|
||||||
|
|
||||||
|
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom);
|
||||||
|
for (BaseRuntimeChildDefinition childDefinition : definition.getChildrenAndExtension()) {
|
||||||
|
if (!inclusionStrategy.test(childDefinition.getElementName())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theFrom);
|
||||||
|
List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTo);
|
||||||
|
|
||||||
|
mergeFields(terser, theTo, childDefinition, theFromFieldValues, theToFieldValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges value of the specified field from <code>theFrom</code> resource to <code>theTo</code> resource. Fields
|
||||||
|
* values are compared via the equalsDeep method, or via object identity if this method is not available.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theFieldName Name of the child filed to merge
|
||||||
|
* @param theFrom Resource to merge the specified field from
|
||||||
|
* @param theTo Resource to merge the specified field into
|
||||||
|
*/
|
||||||
|
public static void mergeField(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
|
mergeField(theFhirContext, theFhirContext.newTerser(), theFieldName, theFrom, theTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges value of the specified field from <code>theFrom</code> resource to <code>theTo</code> resource. Fields
|
||||||
|
* values are compared via the equalsDeep method, or via object identity if this method is not available.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theTerser Terser to be used when cloning the field values
|
||||||
|
* @param theFieldName Name of the child filed to merge
|
||||||
|
* @param theFrom Resource to merge the specified field from
|
||||||
|
* @param theTo Resource to merge the specified field into
|
||||||
|
*/
|
||||||
|
public static void mergeField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
|
BaseRuntimeChildDefinition childDefinition = getBaseRuntimeChildDefinition(theFhirContext, theFieldName, theFrom);
|
||||||
|
|
||||||
|
List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theFrom);
|
||||||
|
List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTo);
|
||||||
|
|
||||||
|
mergeFields(theTerser, theTo, childDefinition, theFromFieldValues, theToFieldValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BaseRuntimeChildDefinition getBaseRuntimeChildDefinition(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom) {
|
||||||
|
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom);
|
||||||
|
BaseRuntimeChildDefinition childDefinition = definition.getChildByName(theFieldName);
|
||||||
|
if (childDefinition == null) {
|
||||||
|
throw new IllegalStateException(String.format("Field %s does not exist", theFieldName));
|
||||||
|
}
|
||||||
|
return childDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mergeFields(FhirTerser theTerser, IBaseResource theTo, BaseRuntimeChildDefinition childDefinition, List<IBase> theFromFieldValues, List<IBase> theToFieldValues) {
|
||||||
|
for (IBase theFromFieldValue : theFromFieldValues) {
|
||||||
|
if (contains(theFromFieldValue, theToFieldValues)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IBase newFieldValue = childDefinition.getChildByName(childDefinition.getElementName()).newInstance();
|
||||||
|
theTerser.cloneInto(theFromFieldValue, newFieldValue, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
theToFieldValues.add(newFieldValue);
|
||||||
|
} catch (UnsupportedOperationException e) {
|
||||||
|
childDefinition.getMutator().setValue(theTo, newFieldValue);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones the specified resource.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theInstance The instance to be cloned
|
||||||
|
* @param <T> Base resource type
|
||||||
|
* @return Returns a cloned instance
|
||||||
|
*/
|
||||||
|
public static <T extends IBaseResource> T clone(FhirContext theFhirContext, T theInstance) {
|
||||||
|
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theInstance.getClass());
|
||||||
|
T retVal = (T) definition.newInstance();
|
||||||
|
|
||||||
|
FhirTerser terser = theFhirContext.newTerser();
|
||||||
|
terser.cloneInto(theInstance, retVal, true);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new element instance
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theElementType Element type name
|
||||||
|
* @param <T> Base element type
|
||||||
|
* @return Returns a new instance of the element
|
||||||
|
*/
|
||||||
|
public static <T extends IBase> T newElement(FhirContext theFhirContext, String theElementType) {
|
||||||
|
BaseRuntimeElementDefinition def = theFhirContext.getElementDefinition(theElementType);
|
||||||
|
return (T) def.newInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new element instance
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theElementType Element type name
|
||||||
|
* @param theConstructorParam Initialization parameter for the element
|
||||||
|
* @param <T> Base element type
|
||||||
|
* @return Returns a new instance of the element with the specified initial value
|
||||||
|
*/
|
||||||
|
public static <T extends IBase> T newElement(FhirContext theFhirContext, String theElementType, Object theConstructorParam) {
|
||||||
|
BaseRuntimeElementDefinition def = theFhirContext.getElementDefinition(theElementType);
|
||||||
|
return (T) def.newInstance(theConstructorParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new resource definition.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theResourceName Name of the resource in the context
|
||||||
|
* @param <T> Type of the resource
|
||||||
|
* @return Returns a new instance of the resource
|
||||||
|
*/
|
||||||
|
public static <T extends IBase> T newResource(FhirContext theFhirContext, String theResourceName) {
|
||||||
|
RuntimeResourceDefinition def = theFhirContext.getResourceDefinition(theResourceName);
|
||||||
|
return (T) def.newInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new resource definition.
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context holding resource definition
|
||||||
|
* @param theResourceName Name of the resource in the context
|
||||||
|
* @param theConstructorParam Initialization parameter for the new instance
|
||||||
|
* @param <T> Type of the resource
|
||||||
|
* @return Returns a new instance of the resource
|
||||||
|
*/
|
||||||
|
public static <T extends IBase> T newResource(FhirContext theFhirContext, String theResourceName, Object theConstructorParam) {
|
||||||
|
RuntimeResourceDefinition def = theFhirContext.getResourceDefinition(theResourceName);
|
||||||
|
return (T) def.newInstance(theConstructorParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Core Library
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper class holding context-related instances, and the resource being operated on. Sample use case is
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* TerserUtilHelper helper = TerserUtilHelper.newHelper(ourFhirContext, "Patient");
|
||||||
|
* helper.setField("identifier.system", "http://org.com/sys");
|
||||||
|
* helper.setField("identifier.value", "123");
|
||||||
|
* ...
|
||||||
|
* Patient patient = helper.getResource();
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public class TerserUtilHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method for creating a new instance of the wrapper
|
||||||
|
*
|
||||||
|
* @param theFhirContext FHIR Context to be used for all further operations
|
||||||
|
* @param theResourceName Name of the resource type
|
||||||
|
* @return Returns a new helper instance
|
||||||
|
*/
|
||||||
|
public static TerserUtilHelper newHelper(FhirContext theFhirContext, String theResourceName) {
|
||||||
|
return newHelper(theFhirContext, (IBaseResource) TerserUtil.newResource(theFhirContext, theResourceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method for creating a new instance of the wrapper
|
||||||
|
*
|
||||||
|
* @param theFhirContext FHIR Context to be used for all further operations
|
||||||
|
* @param theResource The resource to operate on
|
||||||
|
* @return Returns a new helper instance
|
||||||
|
*/
|
||||||
|
public static TerserUtilHelper newHelper(FhirContext theFhirContext, IBaseResource theResource) {
|
||||||
|
TerserUtilHelper retVal = new TerserUtilHelper(theFhirContext, theResource);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FhirContext myContext;
|
||||||
|
private FhirTerser myTerser;
|
||||||
|
private IBaseResource myResource;
|
||||||
|
|
||||||
|
protected TerserUtilHelper(FhirContext theFhirContext, IBaseResource theResource) {
|
||||||
|
myContext = theFhirContext;
|
||||||
|
myResource = theResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets string field at the specified FHIR path
|
||||||
|
*
|
||||||
|
* @param theField The FHIR Path to set the values at
|
||||||
|
* @param theValue The string value to be set
|
||||||
|
* @return Returns current instance
|
||||||
|
*/
|
||||||
|
public TerserUtilHelper setField(String theField, String theValue) {
|
||||||
|
IBase value = newStringElement(theValue);
|
||||||
|
TerserUtil.setFieldByFhirPath(getTerser(), theField, myResource, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets field at the specified FHIR path
|
||||||
|
*
|
||||||
|
* @param theField The FHIR Path to set the values at
|
||||||
|
* @param theValue The value to be set
|
||||||
|
* @return Returns current instance
|
||||||
|
*/
|
||||||
|
public TerserUtilHelper setField(String theField, String theFieldType, Object theValue) {
|
||||||
|
IBase value = newElement(theFieldType, theValue);
|
||||||
|
TerserUtil.setFieldByFhirPath(getTerser(), theField, myResource, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IBase newStringElement(String theValue) {
|
||||||
|
return newElement("string", theValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IBase newElement(String theElementType, Object theValue) {
|
||||||
|
IBase value = TerserUtil.newElement(myContext, theElementType, theValue);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets values of the specified field.
|
||||||
|
*
|
||||||
|
* @param theField The field to get values from
|
||||||
|
* @return Returns a collection of values containing values or null if the spefied field doesn't exist
|
||||||
|
*/
|
||||||
|
public List<IBase> getFieldValues(String theField) {
|
||||||
|
return TerserUtil.getValues(myContext, myResource, theField);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the terser instance, creating one if necessary.
|
||||||
|
*
|
||||||
|
* @return Returns the terser
|
||||||
|
*/
|
||||||
|
public FhirTerser getTerser() {
|
||||||
|
if (myTerser == null) {
|
||||||
|
myTerser = myContext.newTerser();
|
||||||
|
}
|
||||||
|
return myTerser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets resource that this helper operates on
|
||||||
|
*
|
||||||
|
* @param <T> Instance type of the resource
|
||||||
|
* @return Returns the resources
|
||||||
|
*/
|
||||||
|
public <T extends IBaseResource> T getResource() {
|
||||||
|
return (T) myResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets runtime definition for the resource
|
||||||
|
*
|
||||||
|
* @return Returns resource definition.
|
||||||
|
*/
|
||||||
|
public RuntimeResourceDefinition getResourceDefinition() {
|
||||||
|
return myContext.getResourceDefinition(myResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new element
|
||||||
|
*
|
||||||
|
* @param theElementName Name of the element to create
|
||||||
|
* @return Returns a new element
|
||||||
|
*/
|
||||||
|
public IBase newElement(String theElementName) {
|
||||||
|
return TerserUtil.newElement(myContext, theElementName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets context holding resource definition.
|
||||||
|
*
|
||||||
|
* @return Returns the current FHIR context.
|
||||||
|
*/
|
||||||
|
public FhirContext getContext() {
|
||||||
|
return myContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
type: add
|
||||||
|
issue: 2449
|
||||||
|
title: "Adds interceptors for the following functionality:
|
||||||
|
* Data normalization (n11n) - removing unwanted characters (control, etc. as defined by the requirements)
|
||||||
|
* Data standardization (s13n) - normalizing data by ensuring word spacing and character cases are uniform
|
||||||
|
* Data validation - making sure that addresses / emails are validated"
|
|
@ -227,3 +227,52 @@ The UserRequestRetryVersionConflictsInterceptor allows clients to request that t
|
||||||
# JPA Server: Validate Data Being Stored
|
# JPA Server: Validate Data Being Stored
|
||||||
|
|
||||||
The RepositoryValidatingInterceptor can be used to enforce validation rules on data stored in a HAPI FHIR JPA Repository. See [Repository Validating Interceptor](/docs/validation/repository_validating_interceptor.html) for more information.
|
The RepositoryValidatingInterceptor can be used to enforce validation rules on data stored in a HAPI FHIR JPA Repository. See [Repository Validating Interceptor](/docs/validation/repository_validating_interceptor.html) for more information.
|
||||||
|
|
||||||
|
|
||||||
|
# Data Standardization
|
||||||
|
|
||||||
|
```StandardizingInterceptor``` handles data standardization (s13n) requirements. This interceptor applies standardization rules on all FHIR primitives as configured in the ```s13n.json``` file that should be made available on the classpath. This file contains FHIRPath definitions together with the standardizers that should be applied to that path. It comes with six per-build standardizers: NAME_FAMILY, NAME_GIVEN, EMAIL, TITLE, PHONE and TEXT. Custom standardizers can be developed by implementing ```ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.IStandardizer``` interface.
|
||||||
|
|
||||||
|
A sample configuration file can be found below:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Person" : {
|
||||||
|
"Person.name.family" : "NAME_FAMILY",
|
||||||
|
"Person.name.given" : "NAME_GIVEN",
|
||||||
|
"Person.telecom.where(system='phone').value" : "PHONE"
|
||||||
|
},
|
||||||
|
"Patient" : {
|
||||||
|
"name.family" : "NAME_FAMILY",
|
||||||
|
"name.given" : "NAME_GIVEN",
|
||||||
|
"telecom.where(system='phone').value" : "PHONE"
|
||||||
|
},
|
||||||
|
"*" : {
|
||||||
|
"telecom.where(system='email').value" : "org.example.s13n.MyCustomStandardizer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Standardization can be disabled for a given request by providing ```HAPI-Standardization-Disabled: *``` request header. Header value can be any string, it is the presence of the header that disables the s13n.
|
||||||
|
|
||||||
|
|
||||||
|
# Validation: Address Validation
|
||||||
|
|
||||||
|
```AddressValidatingInterceptor``` takes care of validation of addresses on all incoming resources through a 3rd party address validation service. Before a resource containing an Address field is stored, this interceptor invokes address validation service and then stores validation results as an extension on the address with ```https://hapifhir.org/AddressValidation/``` URL.
|
||||||
|
|
||||||
|
This interceptor is configured in ```address-validation.properties``` file that should be made available on the classpath. This file must contain ```validator.class``` property, which defines a fully qualified class implementing ```ca.uhn.fhir.rest.server.interceptor.validation.address.IAddressValidator``` interface. The specified implementation must provide service-specific logic for validating an Address instance. An example implementation can be found in ```ca.uhn.fhir.rest.server.interceptor.validation.address.impl.LoquateAddressValidator``` class which validates addresses by using Loquate Data Cleanse service.
|
||||||
|
|
||||||
|
Address validation can be disabled for a given request by providing ```HAPI-Address-Validation-Disabled: *``` request header. Header value can be any string, it is the presence of the header that disables the validation.
|
||||||
|
|
||||||
|
# Validation: Field-Level Validation
|
||||||
|
|
||||||
|
```FieldValidatingInterceptor``` allows validating primitive fields on various FHIR resources. It expects validation rules to be provided via ```field-validation-rules.json``` file that should be available on the classpath. JSON in this file defines a mapping of FHIRPath expressions to validators that should be applied to those fields. Custom validators that implement ```ca.uhn.fhir.rest.server.interceptor.validation.fields.IValidator``` interface can be provided.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"telecom.where(system='email').value" : "EMAIL",
|
||||||
|
"telecom.where(system='phone').value" : "org.example.validation.MyCustomValidator"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Field validation can be disabled for a given request by providing ```HAPI-Field-Validation-Disabled: *``` request header. Header value can be any string, it is the presence of the header that disables the validation.
|
||||||
|
|
|
@ -21,13 +21,14 @@ package ca.uhn.fhir.mdm.api;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class MdmConstants {
|
public class MdmConstants {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TAG system for Golden Resources which are managed by HAPI MDM.
|
* TAG system for Golden Resources which are managed by HAPI MDM.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public static final String SYSTEM_MDM_MANAGED = "https://hapifhir.org/NamingSystem/managing-mdm-system";
|
public static final String SYSTEM_MDM_MANAGED = "https://hapifhir.org/NamingSystem/managing-mdm-system";
|
||||||
public static final String CODE_HAPI_MDM_MANAGED = "HAPI-MDM";
|
public static final String CODE_HAPI_MDM_MANAGED = "HAPI-MDM";
|
||||||
public static final String DISPLAY_HAPI_MDM_MANAGED = "This Golden Resource can only be modified by Smile CDR's MDM system.";
|
public static final String DISPLAY_HAPI_MDM_MANAGED = "This Golden Resource can only be modified by HAPI MDM system.";
|
||||||
public static final String CODE_NO_MDM_MANAGED = "NO-MDM";
|
public static final String CODE_NO_MDM_MANAGED = "NO-MDM";
|
||||||
public static final String HAPI_ENTERPRISE_IDENTIFIER_SYSTEM = "http://hapifhir.io/fhir/NamingSystem/mdm-golden-resource-enterprise-id";
|
public static final String HAPI_ENTERPRISE_IDENTIFIER_SYSTEM = "http://hapifhir.io/fhir/NamingSystem/mdm-golden-resource-enterprise-id";
|
||||||
public static final String ALL_RESOURCE_SEARCH_PARAM_TYPE = "*";
|
public static final String ALL_RESOURCE_SEARCH_PARAM_TYPE = "*";
|
||||||
|
|
|
@ -73,8 +73,9 @@ public class GoldenResourceHelper {
|
||||||
/**
|
/**
|
||||||
* Creates a copy of the specified resource. This method will carry over resource EID if it exists. If it does not exist,
|
* Creates a copy of the specified resource. This method will carry over resource EID if it exists. If it does not exist,
|
||||||
* a randomly generated UUID EID will be created.
|
* a randomly generated UUID EID will be created.
|
||||||
* @param <T> Supported MDM resource type (e.g. Patient, Practitioner)
|
*
|
||||||
* @param theIncomingResource The resource that will be used as the starting point for the MDM linking.
|
* @param <T> Supported MDM resource type (e.g. Patient, Practitioner)
|
||||||
|
* @param theIncomingResource The resource that will be used as the starting point for the MDM linking.
|
||||||
* @param theMdmTransactionContext
|
* @param theMdmTransactionContext
|
||||||
*/
|
*/
|
||||||
public <T extends IAnyResource> T createGoldenResourceFromMdmSourceResource(T theIncomingResource, MdmTransactionContext theMdmTransactionContext) {
|
public <T extends IAnyResource> T createGoldenResourceFromMdmSourceResource(T theIncomingResource, MdmTransactionContext theMdmTransactionContext) {
|
||||||
|
@ -115,7 +116,7 @@ public class GoldenResourceHelper {
|
||||||
theGoldenResourceIdentifier.getMutator().addValue(theNewGoldenResource, IdentifierUtil.toId(myFhirContext, hapiEid));
|
theGoldenResourceIdentifier.getMutator().addValue(theNewGoldenResource, IdentifierUtil.toId(myFhirContext, hapiEid));
|
||||||
|
|
||||||
// set identifier on the source resource
|
// set identifier on the source resource
|
||||||
TerserUtil.cloneEidIntoResource(myFhirContext, theSourceResource, hapiEid);
|
cloneEidIntoResource(myFhirContext, theSourceResource, hapiEid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cloneAllExternalEidsIntoNewGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier,
|
private void cloneAllExternalEidsIntoNewGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier,
|
||||||
|
@ -130,7 +131,7 @@ public class GoldenResourceHelper {
|
||||||
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystem();
|
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystem();
|
||||||
String baseSystem = system.get().getValueAsString();
|
String baseSystem = system.get().getValueAsString();
|
||||||
if (Objects.equals(baseSystem, mdmSystem)) {
|
if (Objects.equals(baseSystem, mdmSystem)) {
|
||||||
TerserUtil.cloneEidIntoResource(myFhirContext, theGoldenResourceIdentifier, base, theNewGoldenResource);
|
ca.uhn.fhir.util.TerserUtil.cloneEidIntoResource(myFhirContext, theGoldenResourceIdentifier, base, theNewGoldenResource);
|
||||||
ourLog.debug("System {} differs from system in the MDM rules {}", baseSystem, mdmSystem);
|
ourLog.debug("System {} differs from system in the MDM rules {}", baseSystem, mdmSystem);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -235,18 +236,18 @@ public class GoldenResourceHelper {
|
||||||
for (CanonicalEID incomingExternalEid : theIncomingSourceExternalEids) {
|
for (CanonicalEID incomingExternalEid : theIncomingSourceExternalEids) {
|
||||||
if (goldenResourceExternalEids.contains(incomingExternalEid)) {
|
if (goldenResourceExternalEids.contains(incomingExternalEid)) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
|
||||||
TerserUtil.cloneEidIntoResource(myFhirContext, theGoldenResource, incomingExternalEid);
|
|
||||||
}
|
}
|
||||||
|
cloneEidIntoResource(myFhirContext, theGoldenResource, incomingExternalEid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasIdentifier(IBaseResource theResource) {
|
public boolean hasIdentifier(IBaseResource theResource) {
|
||||||
return TerserUtil.hasValues(myFhirContext, theResource, FIELD_NAME_IDENTIFIER);
|
return ca.uhn.fhir.util.TerserUtil.hasValues(myFhirContext, theResource, FIELD_NAME_IDENTIFIER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mergeIndentifierFields(IBaseResource theFromGoldenResource, IBaseResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) {
|
public void mergeIndentifierFields(IBaseResource theFromGoldenResource, IBaseResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) {
|
||||||
TerserUtil.cloneCompositeField(myFhirContext, theFromGoldenResource, theToGoldenResource, FIELD_NAME_IDENTIFIER);
|
ca.uhn.fhir.util.TerserUtil.cloneCompositeField(myFhirContext, theFromGoldenResource, theToGoldenResource,
|
||||||
|
FIELD_NAME_IDENTIFIER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mergeNonIdentiferFields(IBaseResource theFromGoldenResource, IBaseResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) {
|
public void mergeNonIdentiferFields(IBaseResource theFromGoldenResource, IBaseResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) {
|
||||||
|
@ -275,4 +276,20 @@ public class GoldenResourceHelper {
|
||||||
updateGoldenResourceExternalEidFromSourceResource(theGoldenResource, theSourceResource, theMdmTransactionContext);
|
updateGoldenResourceExternalEidFromSourceResource(theGoldenResource, theSourceResource, theMdmTransactionContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones the specified canonical EID into the identifier field on the resource
|
||||||
|
*
|
||||||
|
* @param theFhirContext Context to pull resource definitions from
|
||||||
|
* @param theResourceToCloneInto Resource to set the EID on
|
||||||
|
* @param theEid EID to be set
|
||||||
|
*/
|
||||||
|
public void cloneEidIntoResource(FhirContext theFhirContext, IBaseResource theResourceToCloneInto, CanonicalEID theEid) {
|
||||||
|
// get a ref to the actual ID Field
|
||||||
|
RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResourceToCloneInto);
|
||||||
|
// hapi has 2 metamodels: for children and types
|
||||||
|
BaseRuntimeChildDefinition resourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER);
|
||||||
|
ca.uhn.fhir.util.TerserUtil.cloneEidIntoResource(theFhirContext, resourceIdentifier,
|
||||||
|
IdentifierUtil.toId(theFhirContext, theEid), theResourceToCloneInto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,296 +21,127 @@ package ca.uhn.fhir.mdm.util;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||||
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
|
||||||
import ca.uhn.fhir.mdm.model.CanonicalEID;
|
import ca.uhn.fhir.mdm.model.CanonicalEID;
|
||||||
import ca.uhn.fhir.util.FhirTerser;
|
import ca.uhn.fhir.util.FhirTerser;
|
||||||
import org.hl7.fhir.instance.model.api.IBase;
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static ca.uhn.fhir.mdm.util.GoldenResourceHelper.FIELD_NAME_IDENTIFIER;
|
|
||||||
import static org.slf4j.LoggerFactory.getLogger;
|
import static org.slf4j.LoggerFactory.getLogger;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public final class TerserUtil {
|
public final class TerserUtil {
|
||||||
private static final Logger ourLog = getLogger(TerserUtil.class);
|
private static final Logger ourLog = getLogger(TerserUtil.class);
|
||||||
|
|
||||||
public static final Collection<String> IDS_AND_META_EXCLUDES =
|
public static final Collection<String> IDS_AND_META_EXCLUDES = ca.uhn.fhir.util.TerserUtil.IDS_AND_META_EXCLUDES;
|
||||||
Collections.unmodifiableSet(Stream.of("id", "identifier", "meta").collect(Collectors.toSet()));
|
|
||||||
|
|
||||||
public static final Predicate<String> EXCLUDE_IDS_AND_META = new Predicate<String>() {
|
public static final Predicate<String> EXCLUDE_IDS_AND_META = ca.uhn.fhir.util.TerserUtil.EXCLUDE_IDS_AND_META;
|
||||||
@Override
|
|
||||||
public boolean test(String s) {
|
|
||||||
return !IDS_AND_META_EXCLUDES.contains(s);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final Predicate<String> INCLUDE_ALL = new Predicate<String>() {
|
|
||||||
@Override
|
|
||||||
public boolean test(String s) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private TerserUtil() {
|
private TerserUtil() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones the specified canonical EID into the identifier field on the resource
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
*
|
|
||||||
* @param theFhirContext Context to pull resource definitions from
|
|
||||||
* @param theResourceToCloneInto Resource to set the EID on
|
|
||||||
* @param theEid EID to be set
|
|
||||||
*/
|
|
||||||
public static void cloneEidIntoResource(FhirContext theFhirContext, IBaseResource theResourceToCloneInto, CanonicalEID theEid) {
|
|
||||||
// get a ref to the actual ID Field
|
|
||||||
RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResourceToCloneInto);
|
|
||||||
// hapi has 2 metamodels: for children and types
|
|
||||||
BaseRuntimeChildDefinition resourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER);
|
|
||||||
cloneEidIntoResource(theFhirContext, resourceIdentifier, IdentifierUtil.toId(theFhirContext, theEid), theResourceToCloneInto);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an Child Definition of `identifier`, a R4/DSTU3 EID Identifier, and a new resource, clone the EID into that resources' identifier list.
|
|
||||||
*/
|
*/
|
||||||
public static void cloneEidIntoResource(FhirContext theFhirContext, BaseRuntimeChildDefinition theIdentifierDefinition, IBase theEid, IBase theResourceToCloneEidInto) {
|
public static void cloneEidIntoResource(FhirContext theFhirContext, BaseRuntimeChildDefinition theIdentifierDefinition, IBase theEid, IBase theResourceToCloneEidInto) {
|
||||||
// FHIR choice types - fields within fhir where we have a choice of ids
|
ca.uhn.fhir.util.TerserUtil.cloneEidIntoResource(theFhirContext, theIdentifierDefinition, theEid, theResourceToCloneEidInto);
|
||||||
BaseRuntimeElementCompositeDefinition<?> childIdentifier = (BaseRuntimeElementCompositeDefinition<?>) theIdentifierDefinition.getChildByName(FIELD_NAME_IDENTIFIER);
|
|
||||||
IBase resourceNewIdentifier = childIdentifier.newInstance();
|
|
||||||
|
|
||||||
FhirTerser terser = theFhirContext.newTerser();
|
|
||||||
terser.cloneInto(theEid, resourceNewIdentifier, true);
|
|
||||||
theIdentifierDefinition.getMutator().addValue(theResourceToCloneEidInto, resourceNewIdentifier);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the specified fields has any values
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
*
|
|
||||||
* @param theFhirContext Context holding resource definition
|
|
||||||
* @param theResource Resource to check if the specified field is set
|
|
||||||
* @param theFieldName name of the field to check
|
|
||||||
* @return Returns true if field exists and has any values set, and false otherwise
|
|
||||||
*/
|
*/
|
||||||
public static boolean hasValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) {
|
public static boolean hasValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) {
|
||||||
RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResource);
|
return ca.uhn.fhir.util.TerserUtil.hasValues(theFhirContext, theResource, theFieldName);
|
||||||
BaseRuntimeChildDefinition resourceIdentifier = resourceDefinition.getChildByName(theFieldName);
|
|
||||||
if (resourceIdentifier == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !(resourceIdentifier.getAccessor().getValues(theResource).isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the Values of a specified field.
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
*
|
|
||||||
* @param theFhirContext Context holding resource definition
|
|
||||||
* @param theResource Resource to check if the specified field is set
|
|
||||||
* @param theFieldName name of the field to check
|
|
||||||
* @return Returns true if field exists and has any values set, and false otherwise
|
|
||||||
*/
|
*/
|
||||||
public static List<IBase> getValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) {
|
public static List<IBase> getValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) {
|
||||||
RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResource);
|
return ca.uhn.fhir.util.TerserUtil.getValues(theFhirContext, theResource, theFieldName);
|
||||||
BaseRuntimeChildDefinition resourceIdentifier = resourceDefinition.getChildByName(theFieldName);
|
|
||||||
if (resourceIdentifier == null) {
|
|
||||||
ourLog.info("There is no field named {} in Resource {}", theFieldName, resourceDefinition.getName());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return resourceIdentifier.getAccessor().getValues(theResource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones specified composite field (collection). Composite field values must confirm to the collections
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
* contract.
|
|
||||||
*
|
|
||||||
* @param theFrom Resource to clone the specified filed from
|
|
||||||
* @param theTo Resource to clone the specified filed to
|
|
||||||
* @param field Field name to be copied
|
|
||||||
*/
|
*/
|
||||||
public static void cloneCompositeField(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, String field) {
|
public static void cloneCompositeField(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, String field) {
|
||||||
FhirTerser terser = theFhirContext.newTerser();
|
ca.uhn.fhir.util.TerserUtil.cloneCompositeField(theFhirContext, theFrom, theTo, field);
|
||||||
|
|
||||||
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom);
|
|
||||||
BaseRuntimeChildDefinition childDefinition = definition.getChildByName(field);
|
|
||||||
|
|
||||||
List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theFrom);
|
|
||||||
List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTo);
|
|
||||||
|
|
||||||
for (IBase theFromFieldValue : theFromFieldValues) {
|
|
||||||
if (containsPrimitiveValue(theFromFieldValue, theToFieldValues)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
IBase newFieldValue = childDefinition.getChildByName(field).newInstance();
|
|
||||||
terser.cloneInto(theFromFieldValue, newFieldValue, true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
theToFieldValues.add(newFieldValue);
|
|
||||||
} catch (Exception e) {
|
|
||||||
childDefinition.getMutator().setValue(theTo, newFieldValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean containsPrimitiveValue(IBase theItem, List<IBase> theItems) {
|
|
||||||
PrimitiveTypeEqualsPredicate predicate = new PrimitiveTypeEqualsPredicate();
|
|
||||||
return theItems.stream().anyMatch(i -> {
|
|
||||||
return predicate.test(i, theItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean contains(IBase theItem, List<IBase> theItems) {
|
|
||||||
Method method = null;
|
|
||||||
for (Method m : theItem.getClass().getDeclaredMethods()) {
|
|
||||||
if (m.getName().equals("equalsDeep")) {
|
|
||||||
method = m;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final Method m = method;
|
|
||||||
return theItems.stream().anyMatch(i -> {
|
|
||||||
if (m != null) {
|
|
||||||
try {
|
|
||||||
return (Boolean) m.invoke(theItem, i);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Unable to compare equality via equalsDeep", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return theItem.equals(i);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void mergeAllFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo) {
|
|
||||||
mergeFields(theFhirContext, theFrom, theTo, INCLUDE_ALL);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void replaceFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate<String> inclusionStrategy) {
|
|
||||||
FhirTerser terser = theFhirContext.newTerser();
|
|
||||||
|
|
||||||
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom);
|
|
||||||
for (BaseRuntimeChildDefinition childDefinition : definition.getChildrenAndExtension()) {
|
|
||||||
if (!inclusionStrategy.test(childDefinition.getElementName())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceField(theFrom, theTo, childDefinition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean fieldExists(FhirContext theFhirContext, String theFieldName, IBaseResource theInstance) {
|
|
||||||
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theInstance);
|
|
||||||
return definition.getChildByName(theFieldName) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void replaceField(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
|
||||||
replaceField(theFhirContext, theFhirContext.newTerser(), theFieldName, theFrom, theTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void replaceField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
|
||||||
replaceField(theFrom, theTo, getBaseRuntimeChildDefinition(theFhirContext, theFieldName, theFrom));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void replaceField(IBaseResource theFrom, IBaseResource theTo, BaseRuntimeChildDefinition childDefinition) {
|
|
||||||
childDefinition.getAccessor().getFirstValueOrNull(theFrom).ifPresent(v -> {
|
|
||||||
childDefinition.getMutator().setValue(theTo, v);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void mergeFieldsExceptIdAndMeta(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo) {
|
|
||||||
mergeFields(theFhirContext, theFrom, theTo, EXCLUDE_IDS_AND_META);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void mergeFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate<String> inclusionStrategy) {
|
|
||||||
FhirTerser terser = theFhirContext.newTerser();
|
|
||||||
|
|
||||||
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom);
|
|
||||||
for (BaseRuntimeChildDefinition childDefinition : definition.getChildrenAndExtension()) {
|
|
||||||
if (!inclusionStrategy.test(childDefinition.getElementName())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theFrom);
|
|
||||||
List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTo);
|
|
||||||
|
|
||||||
mergeFields(terser, theTo, childDefinition, theFromFieldValues, theToFieldValues);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges value of the specified field from theFrom resource to theTo resource. Fields values are compared via
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
* the equalsDeep method, or via object identity if this method is not available.
|
*/
|
||||||
*
|
public static void mergeAllFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
* @param theFhirContext
|
ca.uhn.fhir.util.TerserUtil.mergeAllFields(theFhirContext, theFrom, theTo);
|
||||||
* @param theFieldName
|
}
|
||||||
* @param theFrom
|
|
||||||
* @param theTo
|
/**
|
||||||
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
|
*/
|
||||||
|
public static void replaceFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate<String> inclusionStrategy) {
|
||||||
|
ca.uhn.fhir.util.TerserUtil.replaceFields(theFhirContext, theFrom, theTo, inclusionStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
|
*/
|
||||||
|
public static boolean fieldExists(FhirContext theFhirContext, String theFieldName, IBaseResource theInstance) {
|
||||||
|
return ca.uhn.fhir.util.TerserUtil.fieldExists(theFhirContext, theFieldName, theInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
|
*/
|
||||||
|
public static void replaceField(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
|
ca.uhn.fhir.util.TerserUtil.replaceField(theFhirContext, theFieldName, theFrom, theTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
|
*/
|
||||||
|
public static void replaceField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
|
ca.uhn.fhir.util.TerserUtil.replaceField(theFhirContext, theTerser, theFieldName, theFrom, theTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
|
*/
|
||||||
|
public static void mergeFieldsExceptIdAndMeta(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
|
ca.uhn.fhir.util.TerserUtil.mergeFieldsExceptIdAndMeta(theFhirContext, theFrom, theTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
|
*/
|
||||||
|
public static void mergeFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate<String> inclusionStrategy) {
|
||||||
|
ca.uhn.fhir.util.TerserUtil.mergeFields(theFhirContext, theFrom, theTo, inclusionStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
*/
|
*/
|
||||||
public static void mergeField(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
public static void mergeField(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
mergeField(theFhirContext, theFhirContext.newTerser(), theFieldName, theFrom, theTo);
|
ca.uhn.fhir.util.TerserUtil.mergeField(theFhirContext, theFieldName, theFrom, theTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges value of the specified field from theFrom resource to theTo resource. Fields values are compared via
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
* the equalsDeep method, or via object identity if this method is not available.
|
|
||||||
*
|
|
||||||
* @param theFhirContext
|
|
||||||
* @param theTerser
|
|
||||||
* @param theFieldName
|
|
||||||
* @param theFrom
|
|
||||||
* @param theTo
|
|
||||||
*/
|
*/
|
||||||
public static void mergeField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
public static void mergeField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theFrom, IBaseResource theTo) {
|
||||||
BaseRuntimeChildDefinition childDefinition = getBaseRuntimeChildDefinition(theFhirContext, theFieldName, theFrom);
|
ca.uhn.fhir.util.TerserUtil.mergeField(theFhirContext, theTerser, theFieldName, theFrom, theTo);
|
||||||
|
|
||||||
List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theFrom);
|
|
||||||
List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTo);
|
|
||||||
|
|
||||||
mergeFields(theTerser, theTo, childDefinition, theFromFieldValues, theToFieldValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BaseRuntimeChildDefinition getBaseRuntimeChildDefinition(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom) {
|
|
||||||
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom);
|
|
||||||
BaseRuntimeChildDefinition childDefinition = definition.getChildByName(theFieldName);
|
|
||||||
if (childDefinition == null) {
|
|
||||||
throw new IllegalStateException(String.format("Field %s does not exist", theFieldName));
|
|
||||||
}
|
|
||||||
return childDefinition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void mergeFields(FhirTerser theTerser, IBaseResource theTo, BaseRuntimeChildDefinition childDefinition, List<IBase> theFromFieldValues, List<IBase> theToFieldValues) {
|
|
||||||
for (IBase theFromFieldValue : theFromFieldValues) {
|
|
||||||
if (contains(theFromFieldValue, theToFieldValues)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
IBase newFieldValue = childDefinition.getChildByName(childDefinition.getElementName()).newInstance();
|
|
||||||
theTerser.cloneInto(theFromFieldValue, newFieldValue, true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
theToFieldValues.add(newFieldValue);
|
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
childDefinition.getMutator().setValue(theTo, newFieldValue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead
|
||||||
|
*/
|
||||||
public static <T extends IBaseResource> T clone(FhirContext theFhirContext, T theInstance) {
|
public static <T extends IBaseResource> T clone(FhirContext theFhirContext, T theInstance) {
|
||||||
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theInstance.getClass());
|
return ca.uhn.fhir.util.TerserUtil.clone(theFhirContext, theInstance);
|
||||||
T retVal = (T) definition.newInstance();
|
|
||||||
|
|
||||||
FhirTerser terser = theFhirContext.newTerser();
|
|
||||||
terser.cloneInto(theInstance, retVal, true);
|
|
||||||
return retVal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,9 @@ class TerserUtilTest extends BaseR4Test {
|
||||||
@Test
|
@Test
|
||||||
void testCloneFields() {
|
void testCloneFields() {
|
||||||
Patient p1 = buildJohny();
|
Patient p1 = buildJohny();
|
||||||
|
p1.addName().addGiven("Sigizmund");
|
||||||
|
p1.setId("Patient/22");
|
||||||
|
|
||||||
Patient p2 = new Patient();
|
Patient p2 = new Patient();
|
||||||
|
|
||||||
TerserUtil.mergeFieldsExceptIdAndMeta(ourFhirContext, p1, p2);
|
TerserUtil.mergeFieldsExceptIdAndMeta(ourFhirContext, p1, p2);
|
||||||
|
@ -54,7 +57,7 @@ class TerserUtilTest extends BaseR4Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCloneWithNonPrimitves() {
|
void testCloneWithNonPrimitives() {
|
||||||
Patient p1 = new Patient();
|
Patient p1 = new Patient();
|
||||||
Patient p2 = new Patient();
|
Patient p2 = new Patient();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.util.ClasspathUtil;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class ConfigLoader {
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(ConfigLoader.class);
|
||||||
|
public static final String CLASSPATH = "classpath:";
|
||||||
|
|
||||||
|
public static String loadResourceContent(String theResourcePath) {
|
||||||
|
if(theResourcePath.startsWith(CLASSPATH)) {
|
||||||
|
theResourcePath = theResourcePath.substring(CLASSPATH.length());
|
||||||
|
}
|
||||||
|
return ClasspathUtil.loadResource(theResourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Properties loadProperties(String theResourcePath) {
|
||||||
|
String propsString = loadResourceContent(theResourcePath);
|
||||||
|
Properties props = new Properties();
|
||||||
|
try {
|
||||||
|
props.load(new StringReader(propsString));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(String.format("Unable to load properties at %s", theResourcePath), e);
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T loadJson(String theResourcePath, Class<T> theModelClass) {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
try {
|
||||||
|
return mapper.readValue(loadResourceContent(theResourcePath), theModelClass);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(String.format("Unable to parse resource at %s", theResourcePath), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.fhirpath.FhirPathExecutionException;
|
||||||
|
import ca.uhn.fhir.fhirpath.IFhirPath;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.ConfigLoader;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.EmailStandardizer;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.FirstNameStandardizer;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.IStandardizer;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.LastNameStandardizer;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.PhoneStandardizer;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.TextStandardizer;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.TitleStandardizer;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Interceptor
|
||||||
|
public class StandardizingInterceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-defined standardizers
|
||||||
|
*/
|
||||||
|
public enum StandardizationType {
|
||||||
|
NAME_FAMILY, NAME_GIVEN, EMAIL, TITLE, PHONE, TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String STANDARDIZATION_DISABLED_HEADER = "HAPI-Standardization-Disabled";
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(StandardizingInterceptor.class);
|
||||||
|
|
||||||
|
private Map<String, Map<String, String>> myConfig;
|
||||||
|
private Map<String, IStandardizer> myStandardizers = new HashMap<>();
|
||||||
|
|
||||||
|
public StandardizingInterceptor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
ourLog.info("Starting StandardizingInterceptor {}", this);
|
||||||
|
|
||||||
|
myConfig = ConfigLoader.loadJson("classpath:field-s13n-rules.json", Map.class);
|
||||||
|
initStandardizers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StandardizingInterceptor(Map<String, Map<String, String>> theConfig) {
|
||||||
|
super();
|
||||||
|
myConfig = theConfig;
|
||||||
|
initStandardizers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initStandardizers() {
|
||||||
|
myStandardizers.put(StandardizationType.NAME_FAMILY.name(), new LastNameStandardizer());
|
||||||
|
myStandardizers.put(StandardizationType.NAME_GIVEN.name(), new FirstNameStandardizer());
|
||||||
|
myStandardizers.put(StandardizationType.EMAIL.name(), new EmailStandardizer());
|
||||||
|
myStandardizers.put(StandardizationType.TITLE.name(), new TitleStandardizer());
|
||||||
|
myStandardizers.put(StandardizationType.PHONE.name(), new PhoneStandardizer());
|
||||||
|
myStandardizers.put(StandardizationType.TEXT.name(), new TextStandardizer());
|
||||||
|
|
||||||
|
ourLog.info("Initialized standardizers {}", myStandardizers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
||||||
|
public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) {
|
||||||
|
ourLog.debug("Standardizing on pre-create for - {}, {}", theRequest, theResource);
|
||||||
|
standardize(theRequest, theResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
|
||||||
|
public void resourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
|
||||||
|
ourLog.debug("Standardizing on pre-update for - {}, {}, {}", theRequest, theOldResource, theNewResource);
|
||||||
|
standardize(theRequest, theNewResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void standardize(RequestDetails theRequest, IBaseResource theResource) {
|
||||||
|
if (!theRequest.getHeaders(STANDARDIZATION_DISABLED_HEADER).isEmpty()) {
|
||||||
|
ourLog.debug("Standardization for {} is disabled via header {}", theResource, STANDARDIZATION_DISABLED_HEADER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theResource == null) {
|
||||||
|
ourLog.debug("Nothing to standardize for {}", theRequest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FhirContext ctx = theRequest.getFhirContext();
|
||||||
|
|
||||||
|
String resourceType = ctx.getResourceType(theResource);
|
||||||
|
IFhirPath fhirPath = ctx.newFhirPath();
|
||||||
|
|
||||||
|
for (Map.Entry<String, Map<String, String>> rule : myConfig.entrySet()) {
|
||||||
|
String resourceFromConfig = rule.getKey();
|
||||||
|
if (!appliesToResource(resourceFromConfig, resourceType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
standardize(theResource, rule.getValue(), fhirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void standardize(IBaseResource theResource, Map<String, String> theRules, IFhirPath theFhirPath) {
|
||||||
|
for (Map.Entry<String, String> rule : theRules.entrySet()) {
|
||||||
|
IStandardizer std = getStandardizer(rule);
|
||||||
|
List<IBase> values;
|
||||||
|
try {
|
||||||
|
values = theFhirPath.evaluate(theResource, rule.getKey(), IBase.class);
|
||||||
|
} catch (FhirPathExecutionException e) {
|
||||||
|
ourLog.warn("Unable to evaluate path at {} for {}", rule.getKey(), theResource);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (IBase v : values) {
|
||||||
|
if (!(v instanceof IPrimitiveType)) {
|
||||||
|
ourLog.warn("Value at path {} is of type {}, which is not of primitive type - skipping", rule.getKey(), v.fhirType());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IPrimitiveType<?> value = (IPrimitiveType<?>) v;
|
||||||
|
String valueString = value.getValueAsString();
|
||||||
|
String standardizedValueString = std.standardize(valueString);
|
||||||
|
value.setValueAsString(standardizedValueString);
|
||||||
|
ourLog.debug("Standardized {} to {}", valueString, standardizedValueString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IStandardizer getStandardizer(Map.Entry<String, String> rule) {
|
||||||
|
String standardizerName = rule.getValue();
|
||||||
|
if (myStandardizers.containsKey(standardizerName)) {
|
||||||
|
return myStandardizers.get(standardizerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
IStandardizer standardizer;
|
||||||
|
try {
|
||||||
|
standardizer = (IStandardizer) Class.forName(standardizerName).getDeclaredConstructor().newInstance();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(String.format("Unable to create standardizer %s", standardizerName), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
myStandardizers.put(standardizerName, standardizer);
|
||||||
|
return standardizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean appliesToResource(String theResourceFromConfig, String theActualResourceType) {
|
||||||
|
return theResourceFromConfig.equals(theActualResourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Map<String, String>> getConfig() {
|
||||||
|
return myConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfig(Map<String, Map<String, String>> theConfig) {
|
||||||
|
myConfig = theConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, IStandardizer> getStandardizers() {
|
||||||
|
return myStandardizers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStandardizers(Map<String, IStandardizer> theStandardizers) {
|
||||||
|
myStandardizers = theStandardizers;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardizes email addresses by removing whitespace, ISO control characters and applying lower-case to the values.
|
||||||
|
*/
|
||||||
|
public class EmailStandardizer implements IStandardizer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String standardize(String theString) {
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
for (int offset = 0; offset < theString.length(); ) {
|
||||||
|
int codePoint = theString.codePointAt(offset);
|
||||||
|
offset += Character.charCount(codePoint);
|
||||||
|
|
||||||
|
if (Character.isISOControl(codePoint)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Character.isWhitespace(codePoint)) {
|
||||||
|
buf.append(new String(Character.toChars(codePoint)).toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.text.CaseUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardizes first name by capitalizing all characters following a separators (e.g. -, ') and removing noise characters.
|
||||||
|
*/
|
||||||
|
public class FirstNameStandardizer extends TextStandardizer {
|
||||||
|
|
||||||
|
private Set<String> myDelimiters = new HashSet<>();
|
||||||
|
|
||||||
|
public FirstNameStandardizer() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
initializeDelimiters();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void initializeDelimiters() {
|
||||||
|
addDelimiters("-", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected FirstNameStandardizer addDelimiters(String... theDelimiters) {
|
||||||
|
myDelimiters.addAll(Arrays.asList(theDelimiters));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String standardize(String theString) {
|
||||||
|
theString = replaceTranslates(theString);
|
||||||
|
|
||||||
|
return Arrays.stream(theString.split("\\s+"))
|
||||||
|
.map(this::standardizeNameToken)
|
||||||
|
.filter(s -> !StringUtils.isEmpty(s))
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String capitalize(String theString) {
|
||||||
|
if (theString.length() == 0) {
|
||||||
|
return theString;
|
||||||
|
}
|
||||||
|
if (theString.length() == 1) {
|
||||||
|
return theString.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder buf = new StringBuilder(theString.length());
|
||||||
|
buf.append(Character.toUpperCase(theString.charAt(0)));
|
||||||
|
buf.append(theString.substring(1));
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String standardizeNameToken(String theToken) {
|
||||||
|
if (theToken.isEmpty()) {
|
||||||
|
return theToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isDelimitedToken = false;
|
||||||
|
for (String d : myDelimiters) {
|
||||||
|
if (theToken.contains(d)) {
|
||||||
|
isDelimitedToken = true;
|
||||||
|
theToken = standardizeDelimitedToken(theToken, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDelimitedToken) {
|
||||||
|
return theToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
theToken = removeNoise(theToken);
|
||||||
|
theToken = CaseUtils.toCamelCase(theToken, true);
|
||||||
|
return theToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String standardizeDelimitedToken(String theToken, String d) {
|
||||||
|
boolean isTokenTheDelimiter = theToken.equals(d);
|
||||||
|
if (isTokenTheDelimiter) {
|
||||||
|
return theToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
String splitToken = checkForRegexp(d);
|
||||||
|
String[] splits = theToken.split(splitToken);
|
||||||
|
for (int i = 0; i < splits.length; i++) {
|
||||||
|
splits[i] = standardizeNameToken(splits[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String retVal = join(splits, d);
|
||||||
|
if (theToken.startsWith(d)) {
|
||||||
|
retVal = d.concat(retVal);
|
||||||
|
}
|
||||||
|
if (theToken.endsWith(d)) {
|
||||||
|
retVal = retVal.concat(d);
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String join(String[] theSplits, String theDelimiter) {
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
for (int i = 0; i < theSplits.length; i++) {
|
||||||
|
String s = theSplits[i];
|
||||||
|
if (s == null || s.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (buf.length() != 0) {
|
||||||
|
buf.append(theDelimiter);
|
||||||
|
}
|
||||||
|
buf.append(s);
|
||||||
|
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String checkForRegexp(String theExpression) {
|
||||||
|
if (theExpression.equals(".") || theExpression.equals("|")
|
||||||
|
|| theExpression.equals("(") || theExpression.equals(")")) {
|
||||||
|
return "\\".concat(theExpression);
|
||||||
|
}
|
||||||
|
return theExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isDelimiter(String theString) {
|
||||||
|
return myDelimiters.contains(theString);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract for standardizing textual primitives in the FHIR resources.
|
||||||
|
*/
|
||||||
|
public interface IStandardizer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardizes the specified string.
|
||||||
|
*
|
||||||
|
* @param theString String to be standardized
|
||||||
|
* @return Returns a standardized string.
|
||||||
|
*/
|
||||||
|
public String standardize(String theString);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.apache.commons.text.WordUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardizes last names by capitalizing all characters following a separators (e.g. -, '), capitalizing "Mac" and "Mc"
|
||||||
|
* prefixes and keeping name particles in lower case.
|
||||||
|
*/
|
||||||
|
public class LastNameStandardizer extends FirstNameStandardizer {
|
||||||
|
|
||||||
|
private Set<String> myParticles = new HashSet<>(Arrays.asList("van", "der", "ter", "de", "da", "la"));
|
||||||
|
private Set<String> myPrefixes = new HashSet<>(Arrays.asList("mac", "mc"));
|
||||||
|
private Set<String> myPrefixExcludes = new HashSet<>(Arrays.asList("machi"));
|
||||||
|
|
||||||
|
public LastNameStandardizer() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected LastNameStandardizer addDelimiters(String... theDelimiters) {
|
||||||
|
super.addDelimiters(theDelimiters);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String standardizeNameToken(String theToken) {
|
||||||
|
if (theToken.isEmpty()) {
|
||||||
|
return theToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myParticles.contains(theToken.toLowerCase())) {
|
||||||
|
return theToken.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
String retVal = super.standardizeNameToken(theToken);
|
||||||
|
return handlePrefix(retVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String handlePrefix(String theToken) {
|
||||||
|
String lowerCaseToken = theToken.toLowerCase();
|
||||||
|
for (String exclude : myPrefixExcludes) {
|
||||||
|
if (lowerCaseToken.startsWith(exclude)) {
|
||||||
|
return theToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String prefix : myPrefixes) {
|
||||||
|
if (!lowerCaseToken.startsWith(prefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String capitalizedPrefix = WordUtils.capitalize(prefix);
|
||||||
|
String capitalizedSuffix = WordUtils.capitalize(lowerCaseToken.replaceFirst(prefix, ""));
|
||||||
|
return capitalizedPrefix.concat(capitalizedSuffix);
|
||||||
|
}
|
||||||
|
return theToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.ConfigLoader;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Scanner;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class NoiseCharacters {
|
||||||
|
|
||||||
|
private static final int RANGE_THRESHOLD = 150;
|
||||||
|
|
||||||
|
private Set<Integer> myNoiseCharacters = new HashSet<>();
|
||||||
|
private Set<Range> myNoiseCharacterRanges = new HashSet<>();
|
||||||
|
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
public int getSize() {
|
||||||
|
return myNoiseCharacters.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeFromClasspath() {
|
||||||
|
String noiseChars = ConfigLoader.loadResourceContent("classpath:noise-chars.txt");
|
||||||
|
try (Scanner scanner = new Scanner(noiseChars)) {
|
||||||
|
while (scanner.hasNext()) {
|
||||||
|
parse(scanner.nextLine());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNoise(int theChar) {
|
||||||
|
if (myNoiseCharacters.contains(theChar)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Range r : myNoiseCharacterRanges) {
|
||||||
|
if (r.isInRange(theChar)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parse(String theString) {
|
||||||
|
if (theString.contains("-")) {
|
||||||
|
addRange(theString);
|
||||||
|
} else {
|
||||||
|
add(theString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoiseCharacters add(String theLiteral) {
|
||||||
|
myNoiseCharacters.add(toInt(theLiteral));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoiseCharacters addRange(String theRange) {
|
||||||
|
if (!theRange.contains("-")) {
|
||||||
|
throw new IllegalArgumentException(String.format("Invalid range %s", theRange));
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] range = theRange.split("-");
|
||||||
|
if (range.length < 2) {
|
||||||
|
throw new IllegalArgumentException(String.format("Invalid range %s", theRange));
|
||||||
|
}
|
||||||
|
|
||||||
|
addRange(range[0].trim(), range[1].trim());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoiseCharacters addRange(String theLowerBound, String theUpperBound) {
|
||||||
|
int lower = toInt(theLowerBound);
|
||||||
|
int upper = toInt(theUpperBound);
|
||||||
|
|
||||||
|
if (lower > upper) {
|
||||||
|
throw new IllegalArgumentException(String.format("Invalid character range %s-%s", theLowerBound, theUpperBound));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upper - lower >= RANGE_THRESHOLD) {
|
||||||
|
myNoiseCharacterRanges.add(new Range(lower, upper));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = lower; i <= upper; i++) {
|
||||||
|
myNoiseCharacters.add(i);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int toInt(String theLiteral) {
|
||||||
|
if (!theLiteral.startsWith("#x")) {
|
||||||
|
throw new IllegalArgumentException("Unable to parse " + theLiteral);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Integer.parseInt(theLiteral.substring(2), 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardizes phone numbers to fit 123-456-7890 pattern.
|
||||||
|
*/
|
||||||
|
public class PhoneStandardizer implements IStandardizer {
|
||||||
|
|
||||||
|
public static final String PHONE_NUMBER_PATTERN = "(\\d{3})(\\d{3})(\\d+)";
|
||||||
|
public static final String PHONE_NUMBER_REPLACE_PATTERN = "$1-$2-$3";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String standardize(String thePhone) {
|
||||||
|
StringBuilder buf = new StringBuilder(thePhone.length());
|
||||||
|
for (char ch : thePhone.toCharArray()) {
|
||||||
|
if (Character.isDigit(ch)) {
|
||||||
|
buf.append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.toString().replaceFirst(PHONE_NUMBER_PATTERN, PHONE_NUMBER_REPLACE_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
class Range {
|
||||||
|
|
||||||
|
private int myStart;
|
||||||
|
private int myEnd;
|
||||||
|
|
||||||
|
public Range(int theStart, int theEnd) {
|
||||||
|
this.myStart = theStart;
|
||||||
|
this.myEnd = theEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInRange(int theNum) {
|
||||||
|
return theNum >= getStart() && theNum <= getEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStart() {
|
||||||
|
return myStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEnd() {
|
||||||
|
return myEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object theObject) {
|
||||||
|
if (this == theObject) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theObject == null || getClass() != theObject.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Range range = (Range) theObject;
|
||||||
|
return myStart == range.myStart && myEnd == range.myEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(myStart, myEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("[%s, %s]", getStart(), getEnd());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardizes text literals by removing noise characters.
|
||||||
|
*/
|
||||||
|
public class TextStandardizer implements IStandardizer {
|
||||||
|
|
||||||
|
public static final Pattern DIACRITICAL_MARKS = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
|
||||||
|
|
||||||
|
public static final int EXT_ASCII_RANGE_START = 155;
|
||||||
|
public static final int EXT_ASCII_RANGE_END = 255;
|
||||||
|
|
||||||
|
private List<Range> myAllowedExtendedAscii;
|
||||||
|
private Set<Integer> myAllowedNonLetterAndDigitCharacters = new HashSet<>();
|
||||||
|
private NoiseCharacters myNoiseCharacters = new NoiseCharacters();
|
||||||
|
private Map<Integer, Character> myTranslates = new HashMap<>();
|
||||||
|
|
||||||
|
public TextStandardizer() {
|
||||||
|
myNoiseCharacters.initializeFromClasspath();
|
||||||
|
|
||||||
|
initializeAllowedNonLetterAndDigitCharacters();
|
||||||
|
initializeTranslates();
|
||||||
|
initializeAllowedExtendedAscii();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void initializeAllowedNonLetterAndDigitCharacters() {
|
||||||
|
addAllowedNonLetterAndDigitCharacters('.', '\'', ',', '-', '#', '/', '\\', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TextStandardizer addAllowedNonLetterAndDigitCharacters(Character... theCharacters) {
|
||||||
|
myAllowedNonLetterAndDigitCharacters.addAll(asSet(theCharacters));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Set<Integer> asSet(Character... theCharacters) {
|
||||||
|
return Arrays.stream(theCharacters)
|
||||||
|
.map(c -> (int) c)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TextStandardizer addTranslate(int theTranslate, char theMapping) {
|
||||||
|
myTranslates.put(theTranslate, theMapping);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void initializeTranslates() {
|
||||||
|
addTranslate(0x0080, '\''); // PAD
|
||||||
|
addTranslate(0x00A0, ' '); //  
|
||||||
|
addTranslate((int) ' ', ' '); //  
|
||||||
|
addTranslate(0x201C, '"');
|
||||||
|
addTranslate(0x201D, '"');
|
||||||
|
addTranslate(0x2019, ' ');
|
||||||
|
addTranslate(0x2018, ' ');
|
||||||
|
addTranslate(0x02BD, ' ');
|
||||||
|
addTranslate(0x00B4, ' ');
|
||||||
|
addTranslate(0x02DD, '"');
|
||||||
|
addTranslate((int) '–', '-');
|
||||||
|
addTranslate((int) '-', '-');
|
||||||
|
addTranslate((int) '~', '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void initializeAllowedExtendedAscii() {
|
||||||
|
myAllowedExtendedAscii = new ArrayList<>();
|
||||||
|
|
||||||
|
// refer to https://www.ascii-code.com for the codes
|
||||||
|
for (int[] i : new int[][]{{192, 214}, {216, 246}, {248, 255}}) {
|
||||||
|
addAllowedExtendedAsciiRange(i[0], i[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TextStandardizer addAllowedExtendedAsciiRange(int theRangeStart, int theRangeEnd) {
|
||||||
|
myAllowedExtendedAscii.add(new Range(theRangeStart, theRangeEnd));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String standardize(String theString) {
|
||||||
|
theString = replaceTranslates(theString);
|
||||||
|
return removeNoise(theString);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String replaceTranslates(String theString) {
|
||||||
|
StringBuilder buf = new StringBuilder(theString.length());
|
||||||
|
for (char ch : theString.toCharArray()) {
|
||||||
|
if (myTranslates.containsKey((int) ch)) {
|
||||||
|
buf.append(myTranslates.get((int) ch));
|
||||||
|
} else {
|
||||||
|
buf.append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String replaceAccents(String theString) {
|
||||||
|
String string = java.text.Normalizer.normalize(theString, java.text.Normalizer.Form.NFD);
|
||||||
|
return DIACRITICAL_MARKS.matcher(string).replaceAll("");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String removeNoise(String theToken) {
|
||||||
|
StringBuilder token = new StringBuilder(theToken.length());
|
||||||
|
for (int offset = 0; offset < theToken.length(); ) {
|
||||||
|
int codePoint = theToken.codePointAt(offset);
|
||||||
|
offset += Character.charCount(codePoint);
|
||||||
|
|
||||||
|
switch (Character.getType(codePoint)) {
|
||||||
|
case Character.CONTROL: // \p{Cc}
|
||||||
|
case Character.FORMAT: // \p{Cf}
|
||||||
|
case Character.PRIVATE_USE: // \p{Co}
|
||||||
|
case Character.SURROGATE: // \p{Cs}
|
||||||
|
case Character.UNASSIGNED: // \p{Cn}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (!isNoiseCharacter(codePoint)) {
|
||||||
|
token.append(Character.toChars(codePoint));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isTranslate(int theChar) {
|
||||||
|
return myTranslates.containsKey(theChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isNoiseCharacter(int theChar) {
|
||||||
|
if (myAllowedExtendedAscii.stream().anyMatch(r -> r.isInRange(theChar))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean isExtendedAscii = (theChar >= EXT_ASCII_RANGE_START && theChar <= EXT_ASCII_RANGE_END);
|
||||||
|
if (isExtendedAscii) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return myNoiseCharacters.isNoise(theChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class TitleStandardizer extends LastNameStandardizer {
|
||||||
|
|
||||||
|
private Set<String> myExceptions = new HashSet<>(Arrays.asList("EAS", "EPS", "LLC", "LLP", "of", "at", "in", "and"));
|
||||||
|
private Set<String[]> myBiGramExceptions = new HashSet<String[]>();
|
||||||
|
|
||||||
|
public TitleStandardizer() {
|
||||||
|
super();
|
||||||
|
addDelimiters("/", ".", "|", ">", "<", "(", ")", ":", "!");
|
||||||
|
addAllowed('(', ')', '@', ':', '!', '|', '>', '<');
|
||||||
|
myBiGramExceptions.add(new String[] {"'", "s"});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAllowed(char... theCharacter) {
|
||||||
|
for (char ch : theCharacter) {
|
||||||
|
addAllowedExtendedAsciiRange((int) ch, (int) ch);
|
||||||
|
addAllowedNonLetterAndDigitCharacters(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String standardize(String theString) {
|
||||||
|
theString = replaceTranslates(theString);
|
||||||
|
|
||||||
|
return Arrays.stream(theString.split("\\s+"))
|
||||||
|
.map(String::trim)
|
||||||
|
.map(this::standardizeText)
|
||||||
|
.filter(s -> !StringUtils.isEmpty(s))
|
||||||
|
.map(this::checkTitleExceptions)
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> split(String theString) {
|
||||||
|
int cursor = 0;
|
||||||
|
int start = 0;
|
||||||
|
|
||||||
|
List<String> retVal = new ArrayList<>();
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
|
||||||
|
while (cursor < theString.length()) {
|
||||||
|
int codePoint = theString.codePointAt(cursor);
|
||||||
|
cursor += Character.charCount(codePoint);
|
||||||
|
if (isNoiseCharacter(codePoint)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String str = new String(Character.toChars(codePoint));
|
||||||
|
if (isDelimiter(str)) {
|
||||||
|
if (buf.length() != 0) {
|
||||||
|
retVal.add(buf.toString());
|
||||||
|
buf.setLength(0);
|
||||||
|
}
|
||||||
|
retVal.add(str);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.append(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.length() != 0) {
|
||||||
|
retVal.add(buf.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String standardizeText(String theToken) {
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
List<String> parts = split(theToken);
|
||||||
|
|
||||||
|
String prevPart = null;
|
||||||
|
for(String part : parts) {
|
||||||
|
if (isAllText(part)) {
|
||||||
|
part = standardizeNameToken(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
part = checkBiGram(prevPart, part);
|
||||||
|
buf.append(part);
|
||||||
|
prevPart = part;
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String checkBiGram(String thePart0, String thePart1) {
|
||||||
|
for (String[] biGram : myBiGramExceptions) {
|
||||||
|
if (biGram[0].equalsIgnoreCase(thePart0)
|
||||||
|
&& biGram[1].equalsIgnoreCase(thePart1)) {
|
||||||
|
return biGram[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return thePart1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAllText(String thePart) {
|
||||||
|
for (int offset = 0; offset < thePart.length(); ) {
|
||||||
|
int codePoint = thePart.codePointAt(offset);
|
||||||
|
if (!Character.isLetter(codePoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
offset += Character.charCount(codePoint);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String standardizeNameToken(String theToken) {
|
||||||
|
String exception = myExceptions.stream()
|
||||||
|
.filter(s -> s.equalsIgnoreCase(theToken))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (exception != null) {
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.standardizeNameToken(theToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String checkTitleExceptions(String theString) {
|
||||||
|
return myExceptions.stream()
|
||||||
|
.filter(s -> s.equalsIgnoreCase(theString))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(theString);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.ConfigLoader;
|
||||||
|
import ca.uhn.fhir.util.ExtensionUtil;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Interceptor
|
||||||
|
public class AddressValidatingInterceptor {
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(AddressValidatingInterceptor.class);
|
||||||
|
|
||||||
|
public static final String ADDRESS_TYPE_NAME = "Address";
|
||||||
|
public static final String PROPERTY_VALIDATOR_CLASS = "validator.class";
|
||||||
|
|
||||||
|
public static final String ADDRESS_VALIDATION_DISABLED_HEADER = "HAPI-Address-Validation-Disabled";
|
||||||
|
|
||||||
|
private IAddressValidator myAddressValidator;
|
||||||
|
|
||||||
|
private Properties myProperties;
|
||||||
|
|
||||||
|
|
||||||
|
public AddressValidatingInterceptor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
ourLog.info("Starting AddressValidatingInterceptor {}", this);
|
||||||
|
myProperties = ConfigLoader.loadProperties("classpath:address-validation.properties");
|
||||||
|
start(myProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddressValidatingInterceptor(Properties theProperties) {
|
||||||
|
super();
|
||||||
|
start(theProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start(Properties theProperties) {
|
||||||
|
if (!theProperties.containsKey(PROPERTY_VALIDATOR_CLASS)) {
|
||||||
|
ourLog.info("Address validator class is not defined. Validation is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String validatorClassName = theProperties.getProperty(PROPERTY_VALIDATOR_CLASS);
|
||||||
|
Validate.notBlank(validatorClassName, "%s property can not be blank", PROPERTY_VALIDATOR_CLASS);
|
||||||
|
|
||||||
|
ourLog.info("Using address validator {}", validatorClassName);
|
||||||
|
try {
|
||||||
|
Class validatorClass = Class.forName(validatorClassName);
|
||||||
|
IAddressValidator addressValidator;
|
||||||
|
try {
|
||||||
|
addressValidator = (IAddressValidator) validatorClass
|
||||||
|
.getDeclaredConstructor(Properties.class).newInstance(theProperties);
|
||||||
|
} catch (Exception e) {
|
||||||
|
addressValidator = (IAddressValidator) validatorClass.getDeclaredConstructor().newInstance();
|
||||||
|
}
|
||||||
|
setAddressValidator(addressValidator);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Unable to create validator", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
||||||
|
public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) {
|
||||||
|
ourLog.debug("Validating address on for create {}, {}", theResource, theRequest);
|
||||||
|
handleRequest(theRequest, theResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
|
||||||
|
public void resourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
|
||||||
|
ourLog.debug("Validating address on for update {}, {}, {}", theOldResource, theNewResource, theRequest);
|
||||||
|
handleRequest(theRequest, theNewResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handleRequest(RequestDetails theRequest, IBaseResource theResource) {
|
||||||
|
if (getAddressValidator() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!theRequest.getHeaders(ADDRESS_VALIDATION_DISABLED_HEADER).isEmpty()) {
|
||||||
|
ourLog.debug("Address validation is disabled for this request via header");
|
||||||
|
}
|
||||||
|
|
||||||
|
FhirContext ctx = theRequest.getFhirContext();
|
||||||
|
getAddresses(theResource, ctx)
|
||||||
|
.stream()
|
||||||
|
.filter(a -> {
|
||||||
|
return !ExtensionUtil.hasExtension(a, IAddressValidator.ADDRESS_VALIDATION_EXTENSION_URL) ||
|
||||||
|
ExtensionUtil.hasExtension(a, IAddressValidator.ADDRESS_VALIDATION_EXTENSION_URL, IAddressValidator.EXT_UNABLE_TO_VALIDATE);
|
||||||
|
})
|
||||||
|
.forEach(a -> validateAddress(a, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void validateAddress(IBase theAddress, FhirContext theFhirContext) {
|
||||||
|
try {
|
||||||
|
AddressValidationResult validationResult = getAddressValidator().isValid(theAddress, theFhirContext);
|
||||||
|
ourLog.debug("Validated address {}", validationResult);
|
||||||
|
|
||||||
|
ExtensionUtil.setExtension(theFhirContext, theAddress, IAddressValidator.ADDRESS_VALIDATION_EXTENSION_URL,
|
||||||
|
validationResult.isValid() ? IAddressValidator.EXT_VALUE_VALID : IAddressValidator.EXT_VALUE_INVALID);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ourLog.warn("Unable to validate address", ex);
|
||||||
|
ExtensionUtil.setExtension(theFhirContext, theAddress, IAddressValidator.ADDRESS_VALIDATION_EXTENSION_URL, IAddressValidator.EXT_UNABLE_TO_VALIDATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<IBase> getAddresses(IBaseResource theResource, final FhirContext theFhirContext) {
|
||||||
|
RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theResource);
|
||||||
|
|
||||||
|
List<IBase> retVal = new ArrayList<>();
|
||||||
|
for (BaseRuntimeChildDefinition c : definition.getChildren()) {
|
||||||
|
Class childClass = c.getClass();
|
||||||
|
List<IBase> allValues = c.getAccessor()
|
||||||
|
.getValues(theResource)
|
||||||
|
.stream()
|
||||||
|
.filter(v -> ADDRESS_TYPE_NAME.equals(v.getClass().getSimpleName()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
retVal.addAll(allValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (List<IBase>) retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAddressValidator getAddressValidator() {
|
||||||
|
return myAddressValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddressValidator(IAddressValidator theAddressValidator) {
|
||||||
|
this.myAddressValidator = theAddressValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Properties getProperties() {
|
||||||
|
return myProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProperties(Properties theProperties) {
|
||||||
|
myProperties = theProperties;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class AddressValidationException extends RuntimeException {
|
||||||
|
|
||||||
|
public AddressValidationException() {}
|
||||||
|
|
||||||
|
public AddressValidationException(String theMessage) {
|
||||||
|
super(theMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddressValidationException(String theMessage, Throwable theCause) {
|
||||||
|
super(theMessage, theCause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddressValidationException(Throwable theCause) {
|
||||||
|
super(theCause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class AddressValidationResult {
|
||||||
|
|
||||||
|
private boolean myIsValid;
|
||||||
|
private String myValidatedAddressString;
|
||||||
|
private Map<String, String> myValidationResults = new HashMap<>();
|
||||||
|
private String myRawResponse;
|
||||||
|
private IBase myValidatedAddress;
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return myIsValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValid(boolean theIsValid) {
|
||||||
|
this.myIsValid = theIsValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getValidationResults() {
|
||||||
|
return myValidationResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValidationResults(Map<String, String> myValidationResults) {
|
||||||
|
this.myValidationResults = myValidationResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValidatedAddressString() {
|
||||||
|
return myValidatedAddressString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValidatedAddressString(String theValidatedAddressString) {
|
||||||
|
this.myValidatedAddressString = theValidatedAddressString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IBase getValidatedAddress() {
|
||||||
|
return myValidatedAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValidatedAddress(IBase theValidatedAddress) {
|
||||||
|
this.myValidatedAddress = theValidatedAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRawResponse() {
|
||||||
|
return myRawResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRawResponse(String theRawResponse) {
|
||||||
|
this.myRawResponse = theRawResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return
|
||||||
|
" isValid=" + myIsValid +
|
||||||
|
", validatedAddressString='" + myValidatedAddressString + '\'' +
|
||||||
|
", validationResults=" + myValidationResults + '\'' +
|
||||||
|
", rawResponse='" + myRawResponse + '\'' +
|
||||||
|
", myValidatedAddress='" + myValidatedAddress + '\'';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract for validating addresses.
|
||||||
|
*/
|
||||||
|
public interface IAddressValidator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL for validation results that should be placed on addresses
|
||||||
|
*/
|
||||||
|
public static final String ADDRESS_VALIDATION_EXTENSION_URL = "https://hapifhir.org/AddressValidation/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension value confirming that address can be considered valid (it exists and can be traced to the building)
|
||||||
|
*/
|
||||||
|
public static final String EXT_VALUE_VALID = "valid";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension value confirming that address is invalid (doesn't exist)
|
||||||
|
*/
|
||||||
|
public static final String EXT_VALUE_INVALID = "invalid";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension value indicating that address validation was attempted but could not complete successfully
|
||||||
|
*/
|
||||||
|
public static final String EXT_UNABLE_TO_VALIDATE = "not-validated";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates address against a service
|
||||||
|
*
|
||||||
|
* @param theAddress Address to be validated
|
||||||
|
* @param theFhirContext Current FHIR context
|
||||||
|
* @return Returns true in case address represents a valid
|
||||||
|
* @throws AddressValidationException AddressValidationException is thrown in case validation can not be completed successfully.
|
||||||
|
*/
|
||||||
|
AddressValidationResult isValid(IBase theAddress, FhirContext theFhirContext) throws AddressValidationException;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address.impl;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.IAddressValidator;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationException;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationResult;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public abstract class BaseRestfulValidator implements IAddressValidator {
|
||||||
|
|
||||||
|
public static final String PROPERTY_SERVICE_KEY = "service.key";
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(BaseRestfulValidator.class);
|
||||||
|
|
||||||
|
private Properties myProperties;
|
||||||
|
|
||||||
|
protected abstract AddressValidationResult getValidationResult(AddressValidationResult theResult, JsonNode response, FhirContext theFhirContext) throws Exception;
|
||||||
|
|
||||||
|
protected abstract ResponseEntity<String> getResponseEntity(IBase theAddress, FhirContext theFhirContext) throws Exception;
|
||||||
|
|
||||||
|
protected RestTemplate newTemplate() {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseRestfulValidator(Properties theProperties) {
|
||||||
|
myProperties = theProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AddressValidationResult isValid(IBase theAddress, FhirContext theFhirContext) throws AddressValidationException {
|
||||||
|
ResponseEntity<String> entity;
|
||||||
|
try {
|
||||||
|
entity = getResponseEntity(theAddress, theFhirContext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AddressValidationException("Unable to complete address validation web-service call", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError(entity)) {
|
||||||
|
throw new AddressValidationException(String.format("Service returned an error code %s", entity.getStatusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
String responseBody = entity.getBody();
|
||||||
|
ourLog.debug("Validation service returned {}", responseBody);
|
||||||
|
|
||||||
|
AddressValidationResult retVal = new AddressValidationResult();
|
||||||
|
retVal.setRawResponse(responseBody);
|
||||||
|
|
||||||
|
try {
|
||||||
|
JsonNode response = new ObjectMapper().readTree(responseBody);
|
||||||
|
ourLog.debug("Parsed address validator response {}", response);
|
||||||
|
return getValidationResult(retVal, response, theFhirContext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AddressValidationException("Unable to validate the address", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isError(ResponseEntity<String> entity) {
|
||||||
|
return entity.getStatusCode().isError();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Properties getProperties() {
|
||||||
|
return myProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProperties(Properties theProperties) {
|
||||||
|
myProperties = theProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getApiKey() {
|
||||||
|
return getProperties().getProperty(PROPERTY_SERVICE_KEY);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,212 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address.impl;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationException;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationResult;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.helpers.AddressHelper;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.apache.http.entity.ContentType;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For more details regarind the API refer to
|
||||||
|
* <a href="https://www.loqate.com/resources/support/cleanse-api/international-batch-cleanse/">
|
||||||
|
* https://www.loqate.com/resources/support/cleanse-api/international-batch-cleanse/
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public class LoquateAddressValidator extends BaseRestfulValidator {
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(LoquateAddressValidator.class);
|
||||||
|
|
||||||
|
private static final String[] DUPLICATE_FIELDS_IN_ADDRESS_LINES = {"Locality", "AdministrativeArea", "PostalCode"};
|
||||||
|
|
||||||
|
private static final String DATA_CLEANSE_ENDPOINT = "https://api.addressy.com/Cleansing/International/Batch/v1.00/json4.ws";
|
||||||
|
private static final int MAX_ADDRESS_LINES = 8;
|
||||||
|
|
||||||
|
public LoquateAddressValidator(Properties theProperties) {
|
||||||
|
super(theProperties);
|
||||||
|
if (!theProperties.containsKey(PROPERTY_SERVICE_KEY)) {
|
||||||
|
throw new IllegalArgumentException(String.format("Missing service key defined as %s", PROPERTY_SERVICE_KEY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AddressValidationResult getValidationResult(AddressValidationResult theResult, JsonNode response, FhirContext theFhirContext) {
|
||||||
|
if (!response.isArray() || response.size() < 1) {
|
||||||
|
throw new AddressValidationException("Invalid response - expected to get an array of validated addresses");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode firstMatch = response.get(0);
|
||||||
|
if (!firstMatch.has("Matches")) {
|
||||||
|
throw new AddressValidationException("Invalid response - matches are unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode matches = firstMatch.get("Matches");
|
||||||
|
if (!matches.isArray()) {
|
||||||
|
throw new AddressValidationException("Invalid response - expected to get a validated match in the response");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode match = matches.get(0);
|
||||||
|
return toAddressValidationResult(theResult, match, theFhirContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AddressValidationResult toAddressValidationResult(AddressValidationResult theResult, JsonNode theMatch, FhirContext theFhirContext) {
|
||||||
|
theResult.setValid(isValid(theMatch));
|
||||||
|
|
||||||
|
ourLog.debug("Address validation flag {}", theResult.isValid());
|
||||||
|
JsonNode addressNode = theMatch.get("Address");
|
||||||
|
if (addressNode != null) {
|
||||||
|
theResult.setValidatedAddressString(addressNode.asText());
|
||||||
|
}
|
||||||
|
|
||||||
|
ourLog.debug("Validated address string {}", theResult.getValidatedAddressString());
|
||||||
|
theResult.setValidatedAddress(toAddress(theMatch, theFhirContext));
|
||||||
|
return theResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isValid(JsonNode theMatch) {
|
||||||
|
String addressQualityIndex = null;
|
||||||
|
if (theMatch.has("AQI")) {
|
||||||
|
addressQualityIndex = theMatch.get("AQI").asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
ourLog.debug("Address quality index {}", addressQualityIndex);
|
||||||
|
return "A".equals(addressQualityIndex) || "B".equals(addressQualityIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IBase toAddress(JsonNode match, FhirContext theFhirContext) {
|
||||||
|
IBase addressBase = theFhirContext.getElementDefinition("Address").newInstance();
|
||||||
|
|
||||||
|
AddressHelper helper = new AddressHelper(theFhirContext, addressBase);
|
||||||
|
helper.setText(getString(match, "Address"));
|
||||||
|
|
||||||
|
String str = getString(match, "Address1");
|
||||||
|
if (str != null) {
|
||||||
|
helper.addLine(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDuplicateAddressLines(match, helper);
|
||||||
|
|
||||||
|
helper.setCity(getString(match, "Locality"));
|
||||||
|
helper.setState(getString(match, "AdministrativeArea"));
|
||||||
|
helper.setPostalCode(getString(match, "PostalCode"));
|
||||||
|
helper.setCountry(getString(match, "CountryName"));
|
||||||
|
|
||||||
|
return helper.getAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeDuplicateAddressLines(JsonNode match, AddressHelper address) {
|
||||||
|
int lineCount = 1;
|
||||||
|
String addressLine = null;
|
||||||
|
while ((addressLine = getString(match, "Address" + ++lineCount)) != null) {
|
||||||
|
if (isDuplicate(addressLine, match)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
address.addLine(addressLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDuplicate(String theAddressLine, JsonNode theMatch) {
|
||||||
|
for (String s : DUPLICATE_FIELDS_IN_ADDRESS_LINES) {
|
||||||
|
JsonNode node = theMatch.get(s);
|
||||||
|
if (node == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
theAddressLine = theAddressLine.replaceAll(node.asText(), "");
|
||||||
|
}
|
||||||
|
return theAddressLine.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String getString(JsonNode theNode, String theField) {
|
||||||
|
if (!theNode.has(theField)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode field = theNode.get(theField);
|
||||||
|
if (field.asText().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return theNode.get(theField).asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<String> getResponseEntity(IBase theAddress, FhirContext theFhirContext) throws Exception {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType());
|
||||||
|
headers.set(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
|
||||||
|
headers.set(HttpHeaders.USER_AGENT, "SmileCDR");
|
||||||
|
|
||||||
|
String requestBody = getRequestBody(theFhirContext, theAddress);
|
||||||
|
HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
|
||||||
|
return newTemplate().postForEntity(DATA_CLEANSE_ENDPOINT, request, String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getRequestBody(FhirContext theFhirContext, IBase... theAddresses) throws JsonProcessingException {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
ObjectNode rootNode = mapper.createObjectNode();
|
||||||
|
rootNode.put("Key", getApiKey());
|
||||||
|
rootNode.put("Geocode", false);
|
||||||
|
|
||||||
|
ArrayNode addressesArrayNode = mapper.createArrayNode();
|
||||||
|
int i = 0;
|
||||||
|
for (IBase address : theAddresses) {
|
||||||
|
ourLog.debug("Converting {} out of {} addresses", i++, theAddresses.length);
|
||||||
|
ObjectNode addressNode = toJsonNode(address, mapper, theFhirContext);
|
||||||
|
addressesArrayNode.add(addressNode);
|
||||||
|
}
|
||||||
|
rootNode.set("Addresses", addressesArrayNode);
|
||||||
|
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ObjectNode toJsonNode(IBase theAddress, ObjectMapper mapper, FhirContext theFhirContext) {
|
||||||
|
AddressHelper helper = new AddressHelper(theFhirContext, theAddress);
|
||||||
|
ObjectNode addressNode = mapper.createObjectNode();
|
||||||
|
|
||||||
|
int count = 1;
|
||||||
|
for (String s : helper.getMultiple("line")) {
|
||||||
|
addressNode.put("Address" + count, s);
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (count > MAX_ADDRESS_LINES) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addressNode.put("Locality", helper.getCity());
|
||||||
|
addressNode.put("PostalCode", helper.getPostalCode());
|
||||||
|
addressNode.put("Country", helper.getCountry());
|
||||||
|
return addressNode;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address.impl;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationException;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationResult;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.helpers.AddressHelper;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class MelissaAddressValidator extends BaseRestfulValidator {
|
||||||
|
|
||||||
|
public static final String GLOBAL_ADDRESS_VALIDATION_ENDPOINT = "https://address.melissadata.net/v3/WEB/GlobalAddress/doGlobalAddress" +
|
||||||
|
"?id={id}&a1={a1}&a2={a2}&ctry={ctry}&format={format}";
|
||||||
|
|
||||||
|
public MelissaAddressValidator(Properties theProperties) {
|
||||||
|
super(theProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AddressValidationResult getValidationResult(AddressValidationResult theResult, JsonNode theResponse, FhirContext theFhirContext) {
|
||||||
|
Response response = new Response(theResponse);
|
||||||
|
theResult.setValid(response.isValidAddress());
|
||||||
|
theResult.setValidatedAddressString(response.getAddress());
|
||||||
|
return theResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<String> getResponseEntity(IBase theAddress, FhirContext theFhirContext) throws Exception {
|
||||||
|
Map<String, String> requestParams = getRequestParams(theAddress);
|
||||||
|
return newTemplate().getForEntity(GLOBAL_ADDRESS_VALIDATION_ENDPOINT, String.class, requestParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String, String> getRequestParams(IBase theAddress) {
|
||||||
|
AddressHelper helper = new AddressHelper(null, theAddress);
|
||||||
|
|
||||||
|
Map<String, String> requestParams = new HashMap<>();
|
||||||
|
requestParams.put("t", UUID.randomUUID().toString());
|
||||||
|
requestParams.put("id", getApiKey());
|
||||||
|
requestParams.put("a1", helper.getLine());
|
||||||
|
requestParams.put("a2", helper.getParts());
|
||||||
|
requestParams.put("ctry", helper.getCountry());
|
||||||
|
requestParams.put("format", "json");
|
||||||
|
return requestParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Response {
|
||||||
|
private JsonNode root;
|
||||||
|
private JsonNode records;
|
||||||
|
private JsonNode results;
|
||||||
|
|
||||||
|
private List<String> addressErrors = new ArrayList<>();
|
||||||
|
private List<String> addressChange = new ArrayList<>();
|
||||||
|
private List<String> geocodeStatus = new ArrayList<>();
|
||||||
|
private List<String> geocodeError = new ArrayList<>();
|
||||||
|
private List<String> addressVerification = new ArrayList<>();
|
||||||
|
|
||||||
|
public Response(JsonNode theRoot) {
|
||||||
|
root = theRoot;
|
||||||
|
|
||||||
|
// see codes here - http://wiki.melissadata.com/index.php?title=Result_Codes
|
||||||
|
String transmissionResults = root.get("TransmissionResults").asText();
|
||||||
|
if (!StringUtils.isEmpty(transmissionResults)) {
|
||||||
|
geocodeError.add(transmissionResults);
|
||||||
|
throw new AddressValidationException(String.format("Transmission result %s indicate an error with the request - please check API_KEY", transmissionResults));
|
||||||
|
}
|
||||||
|
|
||||||
|
int recordCount = root.get("TotalRecords").asInt();
|
||||||
|
if (recordCount < 1) {
|
||||||
|
throw new AddressValidationException("Expected at least one record in the address validation response");
|
||||||
|
}
|
||||||
|
|
||||||
|
// get first match
|
||||||
|
records = root.get("Records").get(0);
|
||||||
|
results = records.get("Results");
|
||||||
|
|
||||||
|
// full list of response codes is available here
|
||||||
|
// http://wiki.melissadata.com/index.php?title=Result_Code_Details#Global_Address_Verification
|
||||||
|
for (String s : results.asText().split(",")) {
|
||||||
|
if (s.startsWith("AE")) {
|
||||||
|
addressErrors.add(s);
|
||||||
|
} else if (s.startsWith("AC")) {
|
||||||
|
addressChange.add(s);
|
||||||
|
} else if (s.startsWith("GS")) {
|
||||||
|
geocodeStatus.add(s);
|
||||||
|
} else if (s.startsWith("GE")) {
|
||||||
|
geocodeError.add(s);
|
||||||
|
} else if (s.startsWith("AV")) {
|
||||||
|
addressVerification.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValidAddress() {
|
||||||
|
if (!geocodeError.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return addressErrors.isEmpty() && (geocodeStatus.contains("GS05") || geocodeStatus.contains("GS06"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
if (records == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (!records.has("FormattedAddress")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return records.get("FormattedAddress").asText("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.fields;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class EmailValidator implements IValidator {
|
||||||
|
|
||||||
|
private Pattern myEmailPattern = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(String theString) {
|
||||||
|
return myEmailPattern.matcher(theString).matches();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.fields;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.fhirpath.IFhirPath;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.ConfigLoader;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.IAddressValidator;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Interceptor
|
||||||
|
public class FieldValidatingInterceptor {
|
||||||
|
|
||||||
|
public enum ValidatorType {
|
||||||
|
EMAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(FieldValidatingInterceptor.class);
|
||||||
|
|
||||||
|
public static final String VALIDATION_DISABLED_HEADER = "HAPI-Field-Validation-Disabled";
|
||||||
|
|
||||||
|
private IAddressValidator myAddressValidator;
|
||||||
|
|
||||||
|
private Map<String, String> myConfig;
|
||||||
|
|
||||||
|
|
||||||
|
public FieldValidatingInterceptor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
ourLog.info("Starting FieldValidatingInterceptor {}", this);
|
||||||
|
myConfig = ConfigLoader.loadJson("classpath:field-validation-rules.json", Map.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
||||||
|
public void resourcePreCreate(RequestDetails theRequest, IBaseResource theResource) {
|
||||||
|
ourLog.debug("Validating address on create for resource {} / request {}", theResource, theRequest);
|
||||||
|
handleRequest(theRequest, theResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
|
||||||
|
public void resourcePreUpdate(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
|
||||||
|
ourLog.debug("Validating address on update for resource {} / old resource {} / request {}", theOldResource, theNewResource, theRequest);
|
||||||
|
handleRequest(theRequest, theNewResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handleRequest(RequestDetails theRequest, IBaseResource theResource) {
|
||||||
|
if (!theRequest.getHeaders(VALIDATION_DISABLED_HEADER).isEmpty()) {
|
||||||
|
ourLog.debug("Address validation is disabled for this request via header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FhirContext ctx = theRequest.getFhirContext();
|
||||||
|
IFhirPath fhirPath = ctx.newFhirPath();
|
||||||
|
for (Map.Entry<String, String> e : myConfig.entrySet()) {
|
||||||
|
IValidator validator = getValidator(e.getValue());
|
||||||
|
|
||||||
|
List<IPrimitiveType> values = fhirPath.evaluate(theResource, e.getKey(), IPrimitiveType.class);
|
||||||
|
for (IPrimitiveType value : values) {
|
||||||
|
String valueAsString = value.getValueAsString();
|
||||||
|
if (!validator.isValid(valueAsString)) {
|
||||||
|
throw new IllegalArgumentException(String.format("Invalid resource %s", valueAsString));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IValidator getValidator(String theValue) {
|
||||||
|
if (ValidatorType.EMAIL.name().equals(theValue)) {
|
||||||
|
return new EmailValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (IValidator) Class.forName(theValue).getDeclaredConstructor().newInstance();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(String.format("Unable to create validator for %s", theValue), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getConfig() {
|
||||||
|
return myConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfig(Map<String, String> theConfig) {
|
||||||
|
myConfig = theConfig;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.fields;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
public interface IValidator {
|
||||||
|
|
||||||
|
public boolean isValid(String theString);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.helpers;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.util.PropertyModifyingHelper;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for working with FHIR Address element
|
||||||
|
*/
|
||||||
|
public class AddressHelper extends PropertyModifyingHelper {
|
||||||
|
|
||||||
|
public static final String FIELD_LINE = "line";
|
||||||
|
public static final String FIELD_CITY = "city";
|
||||||
|
public static final String FIELD_TEXT = "text";
|
||||||
|
public static final String FIELD_DISTRICT = "district";
|
||||||
|
public static final String FIELD_STATE = "state";
|
||||||
|
public static final String FIELD_POSTAL = "postalCode";
|
||||||
|
public static final String FIELD_COUNTRY = "country";
|
||||||
|
|
||||||
|
public static final String[] FIELD_NAMES = {FIELD_TEXT, FIELD_LINE, FIELD_CITY, FIELD_DISTRICT, FIELD_STATE,
|
||||||
|
FIELD_POSTAL, FIELD_COUNTRY};
|
||||||
|
|
||||||
|
public static final String[] ADDRESS_PARTS = {FIELD_CITY, FIELD_DISTRICT, FIELD_STATE, FIELD_POSTAL};
|
||||||
|
|
||||||
|
public AddressHelper(FhirContext theFhirContext, IBase theBase) {
|
||||||
|
super(theFhirContext, theBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCountry() {
|
||||||
|
return get(FIELD_COUNTRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCity() {
|
||||||
|
return get(FIELD_CITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getState() {
|
||||||
|
return get(FIELD_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPostalCode() {
|
||||||
|
return get(FIELD_POSTAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getText() {
|
||||||
|
return get(FIELD_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCountry(String theCountry) {
|
||||||
|
set(FIELD_COUNTRY, theCountry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCity(String theCity) {
|
||||||
|
set(FIELD_CITY, theCity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setState(String theState) {
|
||||||
|
set(FIELD_STATE, theState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPostalCode(String thePostal) {
|
||||||
|
set(FIELD_POSTAL, thePostal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setText(String theText) {
|
||||||
|
set(FIELD_TEXT, theText);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getParts() {
|
||||||
|
return Arrays.stream(ADDRESS_PARTS)
|
||||||
|
.map(this::get)
|
||||||
|
.filter(s -> !StringUtils.isBlank(s))
|
||||||
|
.collect(Collectors.joining(getDelimiter()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLine() {
|
||||||
|
return get(FIELD_LINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getLines() {
|
||||||
|
return getMultiple(FIELD_LINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddressHelper addLine(String theLine) {
|
||||||
|
set(FIELD_LINE, theLine);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T getAddress() {
|
||||||
|
return (T) getBase();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getFields(FIELD_NAMES);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
#
|
||||||
|
# Validator class indicates validator that should be used by the AddressValidatingInterceptor for validating all
|
||||||
|
# inbound addresses. This class should implement ca.uhn.fhir.rest.server.interceptor.validation.address.IAddressValidator
|
||||||
|
# interface and provide either a default constructor or constructor accepting java.util.Properties. In the later case
|
||||||
|
# this properties will be passed to that constructor.
|
||||||
|
#
|
||||||
|
# Delivered validators include ca.uhn.fhir.rest.server.interceptor.validation.address.impl.LoquateAddressValidator
|
||||||
|
# and ca.uhn.fhir.rest.server.interceptor.validation.address.impl.MelissaAddressValidator. Both require service
|
||||||
|
# key passed as service.key property.
|
||||||
|
#
|
||||||
|
validator.class=
|
||||||
|
service.key=
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"Person" : {
|
||||||
|
"Person.name.family" : "NAME_FAMILY",
|
||||||
|
"Person.name.given" : "NAME_GIVEN",
|
||||||
|
"Person.telecom.where(system='phone').value" : "PHONE"
|
||||||
|
},
|
||||||
|
"Patient" : {
|
||||||
|
"name.family" : "NAME_FAMILY",
|
||||||
|
"name.given" : "NAME_GIVEN",
|
||||||
|
"telecom.where(system='phone').value" : "PHONE"
|
||||||
|
},
|
||||||
|
"*" : {
|
||||||
|
"telecom.where(system='email').value" : "EMAIL"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"telecom.where(system='email').value" : "EMAIL"
|
||||||
|
}
|
|
@ -0,0 +1,375 @@
|
||||||
|
#x0000-#x0022
|
||||||
|
#x0028-#x002A
|
||||||
|
#x003A-#x0040
|
||||||
|
#x005B-#x005D
|
||||||
|
#x005F-#x0060
|
||||||
|
#x007B-#x00A7
|
||||||
|
#x00A9
|
||||||
|
#x00AB-#x00AE
|
||||||
|
#x00B0-#x00B1
|
||||||
|
#x00B6-#x00B7
|
||||||
|
#x00BB
|
||||||
|
#x00BF
|
||||||
|
#x00D7
|
||||||
|
#x00F7
|
||||||
|
#x0237-#x024F
|
||||||
|
#x0358-#x035C
|
||||||
|
#x0370-#x0373
|
||||||
|
#x0376-#x0379
|
||||||
|
#x037B-#x0383
|
||||||
|
#x0387
|
||||||
|
#x038B
|
||||||
|
#x038D
|
||||||
|
#x03A2
|
||||||
|
#x03CF
|
||||||
|
#x03F6
|
||||||
|
#x03FC-#x03FF
|
||||||
|
#x0482
|
||||||
|
#x0487
|
||||||
|
#x04CF
|
||||||
|
#x04F6-#x04F7
|
||||||
|
#x04FA-#x04FF
|
||||||
|
#x0510-#x0530
|
||||||
|
#x0557-#x0558
|
||||||
|
#x055A-#x0560
|
||||||
|
#x0588-#x0590
|
||||||
|
#x05A2
|
||||||
|
#x05BA
|
||||||
|
#x102B
|
||||||
|
#x1033-#x1035
|
||||||
|
#x3097-#x3098
|
||||||
|
#x103A-#x103F
|
||||||
|
#x104A-#x104F
|
||||||
|
#x30A0
|
||||||
|
#x30FB
|
||||||
|
#x105A-#x109F
|
||||||
|
#x3100-#x3104
|
||||||
|
#x10C6-#x10CF
|
||||||
|
#x312D-#x3130
|
||||||
|
#x10F9-#x10FF
|
||||||
|
#x318F-#x3191
|
||||||
|
#x115A-#x115E
|
||||||
|
#x11A3-#x11A7
|
||||||
|
#x3196-#x319F
|
||||||
|
#x11FA-#x11FF
|
||||||
|
#x31B8-#x31EF
|
||||||
|
#x1207
|
||||||
|
#x3200-#x321F
|
||||||
|
#x1247
|
||||||
|
#x322A-#x3250
|
||||||
|
#x1249
|
||||||
|
#x3260-#x327F
|
||||||
|
#x124E-#x124F
|
||||||
|
#x1257
|
||||||
|
#x328A-#x32B0
|
||||||
|
#x1259
|
||||||
|
#x32C0-#x33FF
|
||||||
|
#x125E-#x125F
|
||||||
|
#x4DB6-#x4DFF
|
||||||
|
#x1287
|
||||||
|
#x9FA6-#x9FFF
|
||||||
|
#x1289
|
||||||
|
#x128E-#x128F
|
||||||
|
#xA48D-#xABFF
|
||||||
|
#x12AF
|
||||||
|
#xD7A4-#xF8FF
|
||||||
|
#x12B1
|
||||||
|
#xFA2E-#xFA2F
|
||||||
|
#x12B6-#x12B7
|
||||||
|
#xFA6B-#xFAFF
|
||||||
|
#x12BF
|
||||||
|
#x12C1
|
||||||
|
#xFB07-#xFB12
|
||||||
|
#x12C6-#x12C7
|
||||||
|
#xFB18-#xFB1C
|
||||||
|
#x12CF
|
||||||
|
#xFB29
|
||||||
|
#x12D7
|
||||||
|
#xFB37
|
||||||
|
#x12EF
|
||||||
|
#xFB3D
|
||||||
|
#x130F
|
||||||
|
#xFB3F
|
||||||
|
#x1311
|
||||||
|
#xFB42
|
||||||
|
#x1316-#x1317
|
||||||
|
#xFB45
|
||||||
|
#x131F
|
||||||
|
#xFBB2-#xFBD2
|
||||||
|
#x1347
|
||||||
|
#xFD3E-#xFD4F
|
||||||
|
#x135B-#x1368
|
||||||
|
#xFD90-#xFD91
|
||||||
|
#x137D-#x139F
|
||||||
|
#xFDC8-#xFDEF
|
||||||
|
#x13F5-#x1400
|
||||||
|
#xFDFC-#xFDFF
|
||||||
|
#x166D-#x166E
|
||||||
|
#xFE10-#xFE1F
|
||||||
|
#x1677-#x1680
|
||||||
|
#xFE24-#xFE6F
|
||||||
|
#x169B-#x169F
|
||||||
|
#xFE75
|
||||||
|
#x16EB-#x16FF
|
||||||
|
#xFEFD-#xFEFE
|
||||||
|
#x170D
|
||||||
|
#xFF00-#xFF0F
|
||||||
|
#x1715-#x171F
|
||||||
|
#xFF1A-#xFF20
|
||||||
|
#x1735-#x173F
|
||||||
|
#xFF3B-#xFF3D
|
||||||
|
#x1754-#x175F
|
||||||
|
#xFF3F
|
||||||
|
#x176D
|
||||||
|
#x1771
|
||||||
|
#xFF5B-#xFF65
|
||||||
|
#x1774-#x177F
|
||||||
|
#xFFBF-#xFFC1
|
||||||
|
#x17D4-#x17D6
|
||||||
|
#xFFC8-#xFFC9
|
||||||
|
#x17D8-#x17DB
|
||||||
|
#xFFD0-#xFFD1
|
||||||
|
#x17DE-#x17DF
|
||||||
|
#xFFD8-#xFFD9
|
||||||
|
#x17EA-#x17EF
|
||||||
|
#xFFDD-#xFFE2
|
||||||
|
#x17FA-#x180A
|
||||||
|
#xFFE4-#xFFF8
|
||||||
|
#x180E-#x180F
|
||||||
|
#xFFFC-#xFFFF
|
||||||
|
#x1000C
|
||||||
|
#x181A-#x181F
|
||||||
|
#x10027
|
||||||
|
#x1878-#x187F
|
||||||
|
#x1003B
|
||||||
|
#x18AA-#x18FF
|
||||||
|
#x1003E
|
||||||
|
#x191D-#x191F
|
||||||
|
#x1004E-#x1004F
|
||||||
|
#x192C-#x192F
|
||||||
|
#x1005E-#x1007F
|
||||||
|
#x193C-#x1945
|
||||||
|
#x100FB-#x10106
|
||||||
|
#x196E-#x196F
|
||||||
|
#x10134-#x102FF
|
||||||
|
#x1031F
|
||||||
|
#x1975-#x1CFF
|
||||||
|
#x1D6C-#x1DFF
|
||||||
|
#x1E9C-#x1E9F
|
||||||
|
#x1EFA-#x1EFF
|
||||||
|
#x1F16-#x1F17
|
||||||
|
#x1F1E-#x1F1F
|
||||||
|
#x1F46-#x1F47
|
||||||
|
#x1F4E-#x1F4F
|
||||||
|
#x1F58
|
||||||
|
#x1F5A
|
||||||
|
#x1F5C
|
||||||
|
#x1F5E
|
||||||
|
#x1F7E-#x1F7F
|
||||||
|
#x1FB5
|
||||||
|
#x1FC5
|
||||||
|
#x1FD4-#x1FD5
|
||||||
|
#x1FDC
|
||||||
|
#x1FF0-#x1FF1
|
||||||
|
#x1FF5
|
||||||
|
#x1FFF-#x200B
|
||||||
|
#x2010-#x2029
|
||||||
|
#x202F-#x205F
|
||||||
|
#x2064-#x2069
|
||||||
|
#x2072-#x2073
|
||||||
|
#x207A-#x207E
|
||||||
|
#x208A-#x20CF
|
||||||
|
#x20EB-#x2101
|
||||||
|
#x2103-#x2106
|
||||||
|
#x2108-#x2109
|
||||||
|
#x2114
|
||||||
|
#x2116-#x2118
|
||||||
|
#x211E-#x2123
|
||||||
|
#x2125
|
||||||
|
#x2127
|
||||||
|
#x2129
|
||||||
|
#x212E
|
||||||
|
#x2132
|
||||||
|
#x213A-#x213C
|
||||||
|
#x0ACE-#x0ACF
|
||||||
|
#x0AD1-#x0ADF
|
||||||
|
#x0AE4-#x0AE5
|
||||||
|
#x0AF0-#x0B00
|
||||||
|
#x0B04
|
||||||
|
#x0B0D-#x0B0E
|
||||||
|
#x0B11-#x0B12
|
||||||
|
#x0B29
|
||||||
|
#x0B31
|
||||||
|
#x0B34
|
||||||
|
#x0B3A-#x0B3B
|
||||||
|
#x0B44-#x0B46
|
||||||
|
#x0B49-#x0B4A
|
||||||
|
#x0B4E-#x0B55
|
||||||
|
#x0B58-#x0B5B
|
||||||
|
#x0B5E
|
||||||
|
#x0B62-#x0B65
|
||||||
|
#x0B70
|
||||||
|
#x0B72-#x0B81
|
||||||
|
#x0B84
|
||||||
|
#x0B8B-#x0B8D
|
||||||
|
#x0B91
|
||||||
|
#x0B96-#x0B98
|
||||||
|
#x0B9B
|
||||||
|
#x0B9D
|
||||||
|
#x0BA0-#x0BA2
|
||||||
|
#x0BA5-#x0BA7
|
||||||
|
#x0BAB-#x0BAD
|
||||||
|
#x0BB6
|
||||||
|
#x0BBA-#x0BBD
|
||||||
|
#x0BC3-#x0BC5
|
||||||
|
#x0BC9
|
||||||
|
#x0BCE-#x0BD6
|
||||||
|
#x0BD8-#x0BE6
|
||||||
|
#x0BF3-#x0C00
|
||||||
|
#x0C04
|
||||||
|
#x0C0D
|
||||||
|
#x0C11
|
||||||
|
#x0C29
|
||||||
|
#x0C34
|
||||||
|
#x0C3A-#x0C3D
|
||||||
|
#x0C45
|
||||||
|
#x0C49
|
||||||
|
#x0C4E-#x0C54
|
||||||
|
#x0C57-#x0C5F
|
||||||
|
#x0C62-#x0C65
|
||||||
|
#x0C70-#x0C81
|
||||||
|
#x0C84
|
||||||
|
#x0C8D
|
||||||
|
#x0C91
|
||||||
|
#x0CA9
|
||||||
|
#x0CB4
|
||||||
|
#x0CBA-#x0CBB
|
||||||
|
#x0CC5
|
||||||
|
#x0CC9
|
||||||
|
#x0CCE-#x0CD4
|
||||||
|
#x0CD7-#x0CDD
|
||||||
|
#x0CDF
|
||||||
|
#x0CE2-#x0CE5
|
||||||
|
#x0CF0-#x0D01
|
||||||
|
#x0D04
|
||||||
|
#x0D0D
|
||||||
|
#x0D11
|
||||||
|
#x0D29
|
||||||
|
#x0D3A-#x0D3D
|
||||||
|
#x0D44-#x0D45
|
||||||
|
#x0D49
|
||||||
|
#x0D4E-#x0D56
|
||||||
|
#x0D58-#x0D5F
|
||||||
|
#x0D62-#x0D65
|
||||||
|
#x0D70-#x0D81
|
||||||
|
#x0D84
|
||||||
|
#x0D97-#x0D99
|
||||||
|
#x0DB2
|
||||||
|
#x10324-#x1032F
|
||||||
|
#x1034A-#x1037F
|
||||||
|
#x1039E-#x103FF
|
||||||
|
#x1049E-#x1049F
|
||||||
|
#x104AA-#x107FF
|
||||||
|
#x10806-#x10807
|
||||||
|
#x10809
|
||||||
|
#x10836
|
||||||
|
#x10839-#x1083B
|
||||||
|
#x1083D-#x1083E
|
||||||
|
#x10840-#x1D164
|
||||||
|
#x1D16A-#x1D16C
|
||||||
|
#x1D183-#x1D184
|
||||||
|
#x1D18C-#x1D1A9
|
||||||
|
#x1D1AE-#x1D3FF
|
||||||
|
#x1D455
|
||||||
|
#x1D49D
|
||||||
|
#x1D4A0-#x1D4A1
|
||||||
|
#x1D4A3-#x1D4A4
|
||||||
|
#x1D4A7-#x1D4A8
|
||||||
|
#x1D4AD
|
||||||
|
#x1D4BA
|
||||||
|
#x1D4BC
|
||||||
|
#x1D4C4
|
||||||
|
#x1D506
|
||||||
|
#x1D50B-#x1D50C
|
||||||
|
#x1D515
|
||||||
|
#x1D51D
|
||||||
|
#x1D53A
|
||||||
|
#x1D53F
|
||||||
|
#x1D545
|
||||||
|
#x1D547-#x1D549
|
||||||
|
#x1D551
|
||||||
|
#x1D6A4-#x1D6A7
|
||||||
|
#x1D6C1
|
||||||
|
#x1D6DB
|
||||||
|
#x1D6FB
|
||||||
|
#x1D715
|
||||||
|
#x1D735
|
||||||
|
#x1D74F
|
||||||
|
#x1D76F
|
||||||
|
#x1D789
|
||||||
|
#x0DBC
|
||||||
|
#x0DBE-#x0DBF
|
||||||
|
#x0DC7-#x0DC9
|
||||||
|
#x0DCB-#x0DCE
|
||||||
|
#x0DD5
|
||||||
|
#x0DD7
|
||||||
|
#x0DE0-#x0DF1
|
||||||
|
#x0DF4-#x0E00
|
||||||
|
#x0E3B-#x0E3F
|
||||||
|
#x0E4F
|
||||||
|
#x0E5A-#x0E80
|
||||||
|
#x0E83
|
||||||
|
#x0E85-#x0E86
|
||||||
|
#x0E89
|
||||||
|
#x0E8B-#x0E8C
|
||||||
|
#x0E8E-#x0E93
|
||||||
|
#x0E98
|
||||||
|
#x0EA0
|
||||||
|
#x0EA4
|
||||||
|
#x0EA6
|
||||||
|
#x0EA8-#x0EA9
|
||||||
|
#x0EAC
|
||||||
|
#x0EBA
|
||||||
|
#x0EBE-#x0EBF
|
||||||
|
#x0EC5
|
||||||
|
#x0EC7
|
||||||
|
#x0ECE-#x0ECF
|
||||||
|
#x0EDA-#x0EDB
|
||||||
|
#x0EDE-#x0EFF
|
||||||
|
#x0F01-#x0F17
|
||||||
|
#x0F1A-#x0F1F
|
||||||
|
#x0F34
|
||||||
|
#x0F36
|
||||||
|
#x0F38
|
||||||
|
#x1D7A9
|
||||||
|
#x1D7C3
|
||||||
|
#x1D7CA-#x1D7CD
|
||||||
|
#x1D800-#x1FFFF
|
||||||
|
#x2A6D7-#x2F7FF
|
||||||
|
#x2FA1E-#xE0000
|
||||||
|
#xE0002-#xE001F
|
||||||
|
#xE0080-#xE00FF
|
||||||
|
#x00B4
|
||||||
|
#x0650
|
||||||
|
#x0F3A-#x0F3D
|
||||||
|
#x0F48
|
||||||
|
#x0F6B-#x0F70
|
||||||
|
#x0F85
|
||||||
|
#x0F8C-#x0F8F
|
||||||
|
#x0F98
|
||||||
|
#x0FBD-#x0FC5
|
||||||
|
#x0FC7-#x0FFF
|
||||||
|
#x1022
|
||||||
|
#x1028
|
||||||
|
#x2140-#x2144
|
||||||
|
#x214A-#x2152
|
||||||
|
#x2160-#x245F
|
||||||
|
#x249C-#x24E9
|
||||||
|
#x2500-#x2775
|
||||||
|
#x2794-#x3004
|
||||||
|
#x3007-#x3029
|
||||||
|
#x3030
|
||||||
|
#x3036-#x303A
|
||||||
|
#x303D-#x3040
|
||||||
|
#x0024-#x0025
|
||||||
|
#x002C
|
|
@ -0,0 +1,33 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ConfigLoaderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConfigLoading() {
|
||||||
|
Map config = ConfigLoader.loadJson("classpath:field-s13n-rules.json", Map.class);
|
||||||
|
assertNotNull(config);
|
||||||
|
assertTrue(config.size() > 0);
|
||||||
|
|
||||||
|
Properties props = ConfigLoader.loadProperties("classpath:address-validation.properties");
|
||||||
|
assertNotNull(props);
|
||||||
|
assertTrue(props.size() > 0);
|
||||||
|
|
||||||
|
String text = ConfigLoader.loadResourceContent("classpath:noise-chars.txt");
|
||||||
|
assertNotNull(text);
|
||||||
|
assertTrue(text.length() > 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ConfigLoader.loadResourceContent("blah");
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class EmailStandardizerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStandardization() {
|
||||||
|
IStandardizer std = new EmailStandardizer();
|
||||||
|
assertEquals("thisis_afancy@email.com", std.standardize(" ThisIs_aFancy\n @email.com \t"));
|
||||||
|
assertEquals("емайл@мaйлсервер.ком", std.standardize("\t емайл@мAйлсервер.ком"));
|
||||||
|
assertEquals("show.me.the@moneycom", std.standardize("show . m e . t he@Moneycom"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.fields.EmailValidator;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.fields.IValidator;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class EmailValidatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmailValidation() {
|
||||||
|
IValidator val = new EmailValidator();
|
||||||
|
|
||||||
|
assertTrue(val.isValid("show.me.the.money@email.com"));
|
||||||
|
assertFalse(val.isValid("money@email"));
|
||||||
|
assertFalse(val.isValid("show me the money@email.com"));
|
||||||
|
assertFalse(val.isValid("gimme dough"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class NameStandardizerTest {
|
||||||
|
|
||||||
|
private LastNameStandardizer myLastNameStandardizer = new LastNameStandardizer();
|
||||||
|
private FirstNameStandardizer myFirstNameStandardizer = new FirstNameStandardizer();
|
||||||
|
|
||||||
|
// for rules refer to https://docs.google.com/document/d/1Vz0vYwdDsqu6WrkRyzNiBJDLGmWAej5g/edit#
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCleanNoiseCharacters() {
|
||||||
|
assertEquals("Public", myLastNameStandardizer.standardize("\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020"));
|
||||||
|
assertEquals("This - Text Has /ã Lot # Of Special Characters", myLastNameStandardizer.standardize("\t\r\nThis - ┴\t┬\t├\t─\t┼ text ! has \\\\ /Ã lot # of % special % characters\n"));
|
||||||
|
assertEquals("Nbsp", myLastNameStandardizer.standardize("nbsp \u00A0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFrenchRemains() {
|
||||||
|
assertEquals("Ààââ Ææ", myLastNameStandardizer.standardize(" ÀàÂâ \tÆæ\n "));
|
||||||
|
assertEquals("D D'Équateur", myLastNameStandardizer.standardize("D d'Équateur\n "));
|
||||||
|
assertEquals("Des Idées L'Océan", myLastNameStandardizer.standardize("Des idées\nl'océan\n\n "));
|
||||||
|
assertEquals("Ne T'Arrêtes Pas", myLastNameStandardizer.standardize("Ne\tt'arrêtes\npas\n "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMe() {
|
||||||
|
assertEquals("Tim", myFirstNameStandardizer.standardize("tim ☺ "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNameNormalization() {
|
||||||
|
assertEquals("Tim", myFirstNameStandardizer.standardize(" TIM "));
|
||||||
|
assertEquals("Tim", myFirstNameStandardizer.standardize("tim ☺ "));
|
||||||
|
assertEquals("Tim Berners-Lee", myLastNameStandardizer.standardize(" TiM BeRnErS-lEE\n"));
|
||||||
|
assertEquals("Sara O'Leary", myLastNameStandardizer.standardize("\t\nSAra o'leARy \n\n"));
|
||||||
|
assertEquals("Bill McMaster", myLastNameStandardizer.standardize("\nBILL MCMASTER \n\n"));
|
||||||
|
assertEquals("John MacMaster", myLastNameStandardizer.standardize("\njohn macmASTER \n\n"));
|
||||||
|
assertEquals("Vincent van Gogh", myLastNameStandardizer.standardize("vincent van gogh"));
|
||||||
|
assertEquals("Charles de Gaulle", myLastNameStandardizer.standardize("charles de gaulle\n"));
|
||||||
|
assertEquals("Charles-Gaspard de la Rive", myLastNameStandardizer.standardize("charles-gaspard de la rive"));
|
||||||
|
assertEquals("Niccolò Machiavelli", myLastNameStandardizer.standardize("niccolò machiavelli"));
|
||||||
|
assertEquals("O'Reilly M'Grego D'Angelo MacDonald McFry", myLastNameStandardizer.standardize("o'reilly m'grego d'angelo macdonald mcfry"));
|
||||||
|
assertEquals("Cornelius Vanderbilt", myLastNameStandardizer.standardize("cornelius vanderbilt"));
|
||||||
|
assertEquals("Cornelius Vanderbilt Jr.", myLastNameStandardizer.standardize("cornelius vanderbilt jr."));
|
||||||
|
assertEquals("William Shakespeare", myLastNameStandardizer.standardize("william shakespeare"));
|
||||||
|
assertEquals("Mr. William Shakespeare", myLastNameStandardizer.standardize("mr. william shakespeare"));
|
||||||
|
assertEquals("Amber-Lynn O'Brien", myLastNameStandardizer.standardize("AMBER-LYNN O\u0080<EFBFBD>BRIEN\n"));
|
||||||
|
assertEquals("Noelle Bethea", myLastNameStandardizer.standardize("NOELLE BETHEA\n"));
|
||||||
|
assertEquals("Naomi Anne Ecob", myLastNameStandardizer.standardize("NAOMI ANNE ECOB\n"));
|
||||||
|
assertEquals("Sarah Ann Mary Pollock", myLastNameStandardizer.standardize("SARAH ANN MARY POLLOCK\n"));
|
||||||
|
assertEquals("Tarit Kumar Kanungo", myLastNameStandardizer.standardize("TARIT KUMAR KANUNGO\n"));
|
||||||
|
assertEquals("Tram Anh Thi Nguyen", myLastNameStandardizer.standardize("TRAM ANH THI NGUYEN\n"));
|
||||||
|
assertEquals("William L. Trenwith / Paul J. Trenwith", myLastNameStandardizer.standardize("WILLIAM L. TRENWITH / PAUL J. TRENWITH\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFirstNameNoPrefix() {
|
||||||
|
assertEquals("Mackenzie-Jonah", myFirstNameStandardizer.standardize("MACKENZIE-JONAH"));
|
||||||
|
assertEquals("Charles-Gaspard", myFirstNameStandardizer.standardize("CHARLES-Gaspard"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTranslateMagic() {
|
||||||
|
assertEquals("O'Brien", myLastNameStandardizer.standardize("O\u0080<EFBFBD>BRIEN\n"));
|
||||||
|
assertEquals("O ' Brien", myLastNameStandardizer.standardize("O \u0080<EFBFBD> BRIEN\n"));
|
||||||
|
assertEquals("O 'Brien", myLastNameStandardizer.standardize("O \u0080<EFBFBD>BRIEN\n"));
|
||||||
|
assertEquals("O' Brien", myLastNameStandardizer.standardize("O\u0080 BRIEN\n"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class NoiseCharactersTest {
|
||||||
|
|
||||||
|
private NoiseCharacters myFilter = new NoiseCharacters();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInit() {
|
||||||
|
myFilter.initializeFromClasspath();
|
||||||
|
assertTrue(myFilter.getSize() > 0);
|
||||||
|
|
||||||
|
myFilter = new NoiseCharacters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAdd() {
|
||||||
|
myFilter.add("#x0487");
|
||||||
|
|
||||||
|
char check = (char) Integer.parseInt("487", 16);
|
||||||
|
assertTrue(myFilter.isNoise(check));
|
||||||
|
assertFalse(myFilter.isNoise('A'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddRange() {
|
||||||
|
myFilter.addRange("#x0487-#x0489");
|
||||||
|
|
||||||
|
char check = (char) Integer.parseInt("487", 16);
|
||||||
|
assertTrue(myFilter.isNoise(check));
|
||||||
|
check = (char) Integer.parseInt("488", 16);
|
||||||
|
assertTrue(myFilter.isNoise(check));
|
||||||
|
check = (char) Integer.parseInt("489", 16);
|
||||||
|
assertTrue(myFilter.isNoise(check));
|
||||||
|
|
||||||
|
assertFalse(myFilter.isNoise('A'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddLongRange() {
|
||||||
|
myFilter.addRange("#x0487-#xA489");
|
||||||
|
|
||||||
|
char check = (char) Integer.parseInt("487", 16);
|
||||||
|
assertTrue(myFilter.isNoise(check));
|
||||||
|
check = (char) Integer.parseInt("488", 16);
|
||||||
|
assertTrue(myFilter.isNoise(check));
|
||||||
|
check = (char) Integer.parseInt("489", 16);
|
||||||
|
assertTrue(myFilter.isNoise(check));
|
||||||
|
|
||||||
|
assertFalse(myFilter.isNoise('A'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidChar() {
|
||||||
|
String[] invalidPatterns = new String[]{"", "1", "ABC", "\\u21", "#x0001-#x0000"
|
||||||
|
, "#x0001 - #x - #x0000", "#x0000 #x0022"};
|
||||||
|
|
||||||
|
for (String i : invalidPatterns) {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
myFilter.add(i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
public class PhoneStandardizerTest {
|
||||||
|
|
||||||
|
private IStandardizer myStandardizer = new PhoneStandardizer();
|
||||||
|
|
||||||
|
// for rules refer to https://docs.google.com/document/d/1Vz0vYwdDsqu6WrkRyzNiBJDLGmWAej5g/edit#
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPhoneNumberStandartization() {
|
||||||
|
assertEquals("111-222-3333", myStandardizer.standardize("(111) 222-33-33"));
|
||||||
|
assertEquals("111-222-3333", myStandardizer.standardize("1 1 1 2 2 2 - 3 3 3 3 "));
|
||||||
|
assertEquals("111-222-3", myStandardizer.standardize("111-222-3"));
|
||||||
|
assertEquals("111-222-3", myStandardizer.standardize("111⅕-222-3"));
|
||||||
|
assertEquals("", myStandardizer.standardize(""));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class TextStandardizerTest {
|
||||||
|
|
||||||
|
TextStandardizer myStandardizer = new TextStandardizer();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCleanNoiseCharacters() {
|
||||||
|
assertEquals("public", myStandardizer.standardize("\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020"));
|
||||||
|
assertEquals("textÃ#", myStandardizer.standardize("\t\r\ntext┴\t┬\t├\t─\t┼!\\\\Ã #% \n"));
|
||||||
|
assertEquals("nbsp", myStandardizer.standardize("nbsp \u00A0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCleanBaseAscii() {
|
||||||
|
for (int i = 0; i < 31; i++) {
|
||||||
|
assertEquals("", myStandardizer.standardize(Character.toString((char) i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCleanExtendedAsciiExceptForInternationalSupport() {
|
||||||
|
for (int i = 127; i < 255; i++) {
|
||||||
|
if (!myStandardizer.isNoiseCharacter(i) || myStandardizer.isTranslate(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assertEquals("", myStandardizer.standardize(Character.toString((char) i)), String.format("Expected char #%s to be filtered out", i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExtendedUnicodeSet() {
|
||||||
|
String[] testLiterals = new String[]{
|
||||||
|
".", ".",
|
||||||
|
"𧚓𧚔𧜎𧜏𨩃𨩄𨩅𨩆𨩇𨩈𨩉𨩊𨩋", "𧚓𧚔𧜎𧜏𨩃𨩄𨩅𨩆𨩇𨩈𨩉𨩊𨩋",
|
||||||
|
"𐌁𐌂𐌃𐌄𐌅𐌆𐌇𐌈𐌉𐌊𐌋𐌌𐌍𐌎𐌏𐌐𐌑𐌒𐌓𐌔𐌕𐌖𐌗𐌘𐌙𐌚𐌛𐌜𐌝𐌞", "𐌁𐌂𐌃𐌄𐌅𐌆𐌇𐌈𐌉𐌊𐌋𐌌𐌍𐌎𐌏𐌐𐌑𐌒𐌓𐌔𐌕𐌖𐌗𐌘𐌙𐌚𐌛𐌜𐌝𐌞",
|
||||||
|
"𐌰𐌱𐌲𐌳𐌴𐌵𐌶𐌷𐌸𐌹𐌺𐌻𐌼𐌽𐌾𐌿𐍀𐍁𐍂𐍃𐍄𐍅𐍆𐍇𐍈𐍉𐍊", "𐌰𐌱𐌲𐌳𐌴𐌵𐌶𐌷𐌸𐌹𐌺𐌻𐌼𐌽𐌾𐌿𐍀𐍁𐍂𐍃𐍄𐍅𐍆𐍇𐍈𐍉",
|
||||||
|
"𐎀𐎁𐎂𐎃𐎄𐎅𐎆𐎇𐎈𐎉𐎊𐎋𐎌𐎍𐎎𐎏𐎐𐎑𐎒𐎓𐎔", "𐎀𐎁𐎂𐎃𐎄𐎅𐎆𐎇𐎈𐎉𐎊𐎋𐎌𐎍𐎎𐎏𐎐𐎑𐎒𐎓𐎔",
|
||||||
|
"𐏈𐏉𐏊𐏋𐏌𐏍𐏎𐏏𐏐𐏑𐏒𐏓𐏔𐏕", "",
|
||||||
|
"𐒀𐒁𐒂𐒃𐒄𐒅𐒆𐒇𐒈𐒉𐒊𐒋", "𐒀𐒁𐒂𐒃𐒄𐒅𐒆𐒇𐒈𐒉𐒊𐒋",
|
||||||
|
"𐅄𐅅𐅆𐅇", "",
|
||||||
|
"\uD802\uDD00\uD802\uDD01\uD802\uDD02\uD802\uDD03\uD802\uDD04\uD802\uDD05\uD802\uDD06\uD802\uDD07", "",
|
||||||
|
"\uD802\uDD08\uD802\uDD09\uD802\uDD0A\uD802\uDD0B\uD802\uDD0C\uD802\uDD0D\uD802\uDD0E\uD802\uDD0F", "",
|
||||||
|
"\uD802\uDD10\uD802\uDD11\uD802\uDD12\uD802\uDD13\uD802\uDD14\uD802\uDD15\uD802\uDD16\uD802\uDD17", "",
|
||||||
|
"\uD802\uDD18\uD802\uDD19\uD802\uDD1A", "",
|
||||||
|
"𐒌𐒍𐒎𐒏𐒐𐒑𐒒𐒓𐒔𐒕𐒖𐒗𐒘𐒙𐒚𐒛𐒜𐒝", "𐒌𐒍𐒎𐒏𐒐𐒑𐒒𐒓𐒔𐒕𐒖𐒗𐒘𐒙𐒚𐒛𐒜𐒝",
|
||||||
|
"𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩", "𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩"};
|
||||||
|
|
||||||
|
for (int i = 0; i < testLiterals.length; i += 2) {
|
||||||
|
assertEquals(testLiterals[i + 1], myStandardizer.standardize(testLiterals[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.standardizers;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvFileSource;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
public class TitleStandardizerTest {
|
||||||
|
|
||||||
|
IStandardizer myStandardizer = new TitleStandardizer();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSpecialCases() {
|
||||||
|
assertEquals("Prescribeit", myStandardizer.standardize("prescribeit"));
|
||||||
|
assertEquals("Meddialog", myStandardizer.standardize("MEDDIALOG"));
|
||||||
|
assertEquals("20 / 20", myStandardizer.standardize("20 / 20"));
|
||||||
|
assertEquals("20/20", myStandardizer.standardize("20/20"));
|
||||||
|
assertEquals("L.L.P.", myStandardizer.standardize("L.L.P."));
|
||||||
|
assertEquals("Green Tractors Clow Farm Equipment/", myStandardizer.standardize("GREEN TRACTORS CLOW FARM EQUIPMENT/"));
|
||||||
|
assertEquals("Agilec - Barrie/Orillia (EPS)", myStandardizer.standardize("Agilec - Barrie/Orillia (EPS)"));
|
||||||
|
assertEquals("Clement's/Callander Ida Pharmacies", myStandardizer.standardize("CLEMENT'S/CALLANDER IDA PHARMACIES"));
|
||||||
|
assertEquals("Longley/Vickar L.L.P. Barristers & Solicitors", myStandardizer.standardize("LONGLEY/VICKAR L.L.P. BARRISTERS & SOLICITORS"));
|
||||||
|
assertEquals("-Blan", myStandardizer.standardize("~Blan"));
|
||||||
|
assertEquals("The (C/O Dr Mary Cooke)", myStandardizer.standardize("THE (C/O DR MARY COOKE)"));
|
||||||
|
assertEquals("Sarah Ann Mary Pollock", myStandardizer.standardize("SARAH ANN MARY POLLOCK"));
|
||||||
|
assertEquals("Voir...Être Vu! Opticiens", myStandardizer.standardize("VOIR...ÊTRE VU! OPTICIENS"));
|
||||||
|
assertEquals("Back in Sync: Wellness Centre", myStandardizer.standardize("BACK IN SYNC: WELLNESS CENTRE"));
|
||||||
|
assertEquals("Pearle Vision 9861 (Orchard Park S/C)", myStandardizer.standardize("PEARLE VISION 9861 (ORCHARD PARK S/C)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvFileSource(resources = "/organization_titles.csv", numLinesToSkip = 0)
|
||||||
|
public void testTitleOrganizationsStandardization(String theExpected, String theInput) {
|
||||||
|
String standardizedTitle = myStandardizer.standardize(theInput);
|
||||||
|
assertEquals(theExpected, standardizedTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
The tests for AddressHelper, StandardizingInterceptor, AddressValidatingInterceptor, FieldValidatingInterceptor
|
||||||
|
and TerserUtils are in "hapi-fhir-structures-r4" to avoid issues with dependencies.
|
|
@ -0,0 +1,188 @@
|
||||||
|
1328746 Ont Ltd / Independent Taxi,1328746 ONT LTD / INDEPENDENT TAXI
|
||||||
|
20/20 Massage,20/20 Massage
|
||||||
|
20/20 Optometrists,20/20 OPTOMETRISTS
|
||||||
|
20/20 Vision,20/20 VISION
|
||||||
|
20/20 Vision Care,20/20 VISION CARE
|
||||||
|
20/20 Vision Care Inc.,20/20 VISION CARE INC.
|
||||||
|
20/20 Vision Clinic Inc,20/20 VISION CLINIC INC
|
||||||
|
2189450 Alberta Ltd O/A Revive & Rekindle Massage Therapy,2189450 ALBERTA LTD O/A REVIVE & REKINDLE MASSAGE THERAPY
|
||||||
|
2343571 Ontario Inc O/A Westmore Wellness Rehab Clinic,2343571 ONTARIO INC O/A WESTMORE WELLNESS REHAB CLINIC
|
||||||
|
497084 Ontario Ltd. O/A Woodys Wheels,497084 ONTARIO LTD. O/A WOODYS WHEELS
|
||||||
|
Achilles Orthopaedic Shoes/Medical Devices,ACHILLES ORTHOPAEDIC SHOES/MEDICAL DEVICES
|
||||||
|
Action Sport Physio Saint-Eustache/Deux-Montagnes,ACTION SPORT PHYSIO SAINT-EUSTACHE/DEUX-MONTAGNES
|
||||||
|
Agilec - Barrie/Orillia (EPS),AGILEC - BARRIE/ORILLIA (EPS)
|
||||||
|
Agilec - Guelph/Kitchener/Waterloo (EAS),AGILEC - GUELPH/KITCHENER/WATERLOO (EAS)
|
||||||
|
Agilec - Guelph/Kitchener/Waterloo (EPS),AGILEC - GUELPH/KITCHENER/WATERLOO (EPS)
|
||||||
|
Ags Rehab Solutions - Halton/Peel,AGS REHAB SOLUTIONS - HALTON/PEEL
|
||||||
|
Ags Rehab Solutions Inc - Barrie/Orillia,AGS REHAB SOLUTIONS INC - BARRIE/ORILLIA
|
||||||
|
Ags Rehab Solutions Inc - Thunder Bay/Dryden/Kenora,AGS REHAB SOLUTIONS INC - THUNDER BAY/DRYDEN/KENORA
|
||||||
|
Ags Rehab Solutions Inc-Guelph/Kitchener/Waterloo,AGS REHAB SOLUTIONS INC-GUELPH/KITCHENER/WATERLOO
|
||||||
|
Aladdin Geleidi O/A 1296864 Alberta Ltd,ALADDIN GELEIDI O/A 1296864 ALBERTA LTD
|
||||||
|
Allan McGavin Sports Medicine Centre @ Usb,ALLAN MCGAVIN SPORTS MEDICINE CENTRE @ USB
|
||||||
|
Allan McGavin Sports Medicine Centre Physiotherapy @ Plaza of Nations,ALLAN MCGAVIN SPORTS MEDICINE CENTRE PHYSIOTHERAPY @ PLAZA OF NATIONS
|
||||||
|
Amber-Lynn O'Brien,amber-lynn O<4F>brien
|
||||||
|
Angela Wing Wen Lam,ANGELA WING WEN LAM
|
||||||
|
Ascenseurs Lumar/Concord Quebec Inc.,ASCENSEURS LUMAR/CONCORD QUEBEC INC.
|
||||||
|
Back in Sync: Wellness Centre,BACK IN SYNC: WELLNESS CENTRE
|
||||||
|
Balance: Psychology and Brain Health,BALANCE: PSYCHOLOGY AND BRAIN HEALTH
|
||||||
|
Bayshore Therapy and Rehab - Barrie/Orillia,BAYSHORE THERAPY AND REHAB - BARRIE/ORILLIA
|
||||||
|
Bayshore Therapy and Rehab - Guelph/Kitchener/Waterloo,BAYSHORE THERAPY AND REHAB - GUELPH/KITCHENER/WATERLOO
|
||||||
|
Bayshore Therapy and Rehab - Halton/Peel,BAYSHORE THERAPY AND REHAB - HALTON/PEEL
|
||||||
|
Bayshore Therapy and Rehab - Thunder Bay/Dryden/Kenora,BAYSHORE THERAPY AND REHAB - THUNDER BAY/DRYDEN/KENORA
|
||||||
|
Bing Siang Gan,BING SIANG GAN
|
||||||
|
Bio-Ped/2059779 Ontario Ltd,BIO-PED/2059779 ONTARIO LTD
|
||||||
|
Bloor : Keele Chiropractic,BLOOR : KEELE CHIROPRACTIC
|
||||||
|
Brampton/Bramalea Kwik Kab,BRAMPTON/BRAMALEA KWIK KAB
|
||||||
|
Brock Community Health Centre/Nursing,BROCK COMMUNITY HEALTH CENTRE/NURSING
|
||||||
|
C'Est la Vue !,C'EST LA VUE !
|
||||||
|
Can-Weld/Can-Fab,CAN-WELD/CAN-FAB
|
||||||
|
Cbi - Langley / Oasis Sports Injury Centre,CBI - LANGLEY / OASIS SPORTS INJURY CENTRE
|
||||||
|
Centre de Sante Communitaire Hamilton/Niagara,CENTRE DE SANTE COMMUNITAIRE HAMILTON/NIAGARA
|
||||||
|
Cheelcare / 9302204 Canada Inc.,CHEELCARE / 9302204 CANADA INC.
|
||||||
|
Clement's/Callander Ida Pharmacies,CLEMENT'S/CALLANDER IDA PHARMACIES
|
||||||
|
Clsc/Chsld Des Etchemin,CLSC/CHSLD DES ETCHEMIN
|
||||||
|
Cowichan Eyecare-Chemainus,COWICHAN EYECARE~CHEMAINUS
|
||||||
|
Crawford Healthcare Management - Guelph/Kitchener,CRAWFORD HEALTHCARE MANAGEMENT - GUELPH/KITCHENER
|
||||||
|
Daniel Man Tat Wong,DANIEL MAN TAT WONG
|
||||||
|
Dominique Nadon Ssuo/Uohs,DOMINIQUE NADON SSUO/UOHS
|
||||||
|
Dormez-Vous / Sleep Country,DORMEZ-VOUS / SLEEP COUNTRY
|
||||||
|
Dr Todd E Mazzuca O/A Northstone Chiropractic Paris St,DR TODD E MAZZUCA O/A NORTHSTONE CHIROPRACTIC PARIS ST
|
||||||
|
Dr. Atoosa Chiropractor/Acupuncture,DR. ATOOSA CHIROPRACTOR/ACUPUNCTURE
|
||||||
|
Dufferin Drug Mart O/A Pharmadx Drugs Ltd,DUFFERIN DRUG MART O/A PHARMADX DRUGS LTD
|
||||||
|
Emergency Assoc/Univ of Roc,EMERGENCY ASSOC/UNIV OF ROC
|
||||||
|
Eric Johnson/Hear at Last,ERIC JOHNSON/HEAR AT LAST
|
||||||
|
Eyemate Vision/Crystal Clear Optical,EYEMATE VISION/CRYSTAL CLEAR OPTICAL
|
||||||
|
Eyes Inspire / Visionworks,EYES INSPIRE / VISIONWORKS
|
||||||
|
Feet First Pedorthic/Nursing Foot Care Clinic,FEET FIRST PEDORTHIC/NURSING FOOT CARE CLINIC
|
||||||
|
Fine + Well: Health and Chiropractic,FINE + WELL: HEALTH AND CHIROPRACTIC
|
||||||
|
Foot Solutions/2288564 Ontario Inc,FOOT SOLUTIONS/2288564 ONTARIO INC
|
||||||
|
Friuli Benevolent Corp./Friuli Terrace,FRIULI BENEVOLENT CORP./FRIULI TERRACE
|
||||||
|
Go! Physiotherapy Sports and Wellness Centre,GO! PHYSIOTHERAPY SPORTS AND WELLNESS CENTRE
|
||||||
|
Gobi Ratnaswami Ganapathy,GOBI RATNASWAMI GANAPATHY
|
||||||
|
Gordon Josephson/Gilmour Psychological Services,GORDON JOSEPHSON/GILMOUR PSYCHOLOGICAL SERVICES
|
||||||
|
Grand River Hospital - Kitchener/Waterloo Health Centre,GRAND RIVER HOSPITAL - KITCHENER/WATERLOO HEALTH CENTRE
|
||||||
|
Green Tractors Clow Farm Equipment/,GREEN TRACTORS CLOW FARM EQUIPMENT/
|
||||||
|
H/Q Healthquest,H/Q HEALTHQUEST
|
||||||
|
Hear at Last/Phc Canada Scarborough,HEAR AT LAST/PHC CANADA SCARBOROUGH
|
||||||
|
Hearinglife - Sydney,HEARINGLIFE – SYDNEY
|
||||||
|
Howard Chung C/O Lishan Management Co. Ltd,HOWARD CHUNG C/O LISHAN MANAGEMENT CO. LTD
|
||||||
|
Ian Gray/ Sound Ideas Audiology,IAN GRAY/ SOUND IDEAS AUDIOLOGY
|
||||||
|
Iris-292-Centre Piazzazzurri,IRIS-292-CENTRE PIAZZ`AZZURRI
|
||||||
|
Ismp/Tanya Armstrong,ISMP/TANYA ARMSTRONG
|
||||||
|
Jeffrey Thomas Hovey,JEFFREY THOMAS HOVEY
|
||||||
|
John D Franks/Access Hearing Care,JOHN D FRANKS/ACCESS HEARING CARE
|
||||||
|
John David Jacques Bender,JOHN DAVID JACQUES BENDER
|
||||||
|
Kenneth Bernard Sabourin,KENNETH BERNARD SABOURIN
|
||||||
|
Killaloe Supermarket/Aj's Killaloe Convenience,KILLALOE SUPERMARKET/AJ'S KILLALOE CONVENIENCE
|
||||||
|
Lakeshore General Hospitalc/Oaccounting Dept,LAKESHORE GENERAL HOSPITALC/OACCOUNTING DEPT
|
||||||
|
Le Groupe Forget/Audioprothesistes,LE GROUPE FORGET/AUDIOPROTHESISTES
|
||||||
|
Leaps and Bounds: Performance Rehabilitation,LEAPS AND BOUNDS: PERFORMANCE REHABILITATION
|
||||||
|
Lifemark Health Corp - Barrie/Orillia,LIFEMARK HEALTH CORP - BARRIE/ORILLIA
|
||||||
|
Lifemark Health Corp - Guelph/Kitchener/Waterloo,LIFEMARK HEALTH CORP - GUELPH/KITCHENER/WATERLOO
|
||||||
|
Lifemark Health Corp - Halton / Peel,LIFEMARK HEALTH CORP - HALTON / PEEL
|
||||||
|
Listenup! Canada - Dundas West,LISTENUP! CANADA - DUNDAS WEST
|
||||||
|
Listenup! Canada - Toronto,LISTENUP! CANADA - TORONTO
|
||||||
|
Longley/Vickar L.L.P. Barristers & Solicitors,LONGLEY/VICKAR L.L.P. BARRISTERS & SOLICITORS
|
||||||
|
Lorraine McLeod O/A Union Taxi,LORRAINE MCLEOD O/A UNION TAXI
|
||||||
|
Manpreet Birring,MANPREET BIRRING
|
||||||
|
March of Dimes Canada - Barrie/Orillia (EAS),MARCH OF DIMES CANADA - BARRIE/ORILLIA (EAS)
|
||||||
|
March of Dimes Canada - Barrie/Orillia (EPS),MARCH OF DIMES CANADA - BARRIE/ORILLIA (EPS)
|
||||||
|
March of Dimes Canada - Guelph/Kitchener/Waterloo (EAS),MARCH OF DIMES CANADA - GUELPH/KITCHENER/WATERLOO (EAS)
|
||||||
|
March of Dimes Canada - Guelph/Kitchener/Waterloo (EPS),MARCH OF DIMES CANADA - GUELPH/KITCHENER/WATERLOO (EPS)
|
||||||
|
March of Dimes Canada - Halton/Peel (EAS),MARCH OF DIMES CANADA - HALTON/PEEL (EAS)
|
||||||
|
March of Dimes Canada - Halton/Peel (EPS),MARCH OF DIMES CANADA - HALTON/PEEL (EPS)
|
||||||
|
March of Dimes Canada - Thunder Bay Dryden/Kenora (EAS),MARCH OF DIMES CANADA - THUNDER BAY DRYDEN/KENORA (EAS)
|
||||||
|
March of Dimes Canada - Thunder Bay Dryden/Kenora (EPS),MARCH OF DIMES CANADA - THUNDER BAY DRYDEN/KENORA (EPS)
|
||||||
|
Marion Baechler,MARION BAECHLER
|
||||||
|
Markham Family Medicine Teaching Unit/Health For All Fht,MARKHAM FAMILY MEDICINE TEACHING UNIT/HEALTH FOR ALL FHT
|
||||||
|
Med-E-Ox/Mobility in Motion,MED-E-OX/MOBILITY IN MOTION
|
||||||
|
Medcare Clinics @ Walmart Pen Centre,MEDCARE CLINICS @ WALMART PEN CENTRE
|
||||||
|
Medical Center For Foot/Ankle,MEDICAL CENTER FOR FOOT/ANKLE
|
||||||
|
Mend|Rx Bedford,MEND|RX BEDFORD
|
||||||
|
Michael David Williams,MICHAEL DAVID WILLIAMS
|
||||||
|
Michelle Harvey @ Twelfth Avenue Acupuncture and Herb Clinic,MICHELLE HARVEY @ TWELFTH AVENUE ACUPUNCTURE AND HERB CLINIC
|
||||||
|
Minister of Finance C/O Ministry of Health,MINISTER OF FINANCE C/O MINISTRY OF HEALTH
|
||||||
|
Miracle Ear/Fred Hawkins,MIRACLE EAR/FRED HAWKINS
|
||||||
|
Mount Sinai Rehab C/O Mt Sinai Hosp,MOUNT SINAI REHAB C/O MT SINAI HOSP
|
||||||
|
Moving Along... Your !,MOVING ALONG... YOUR !
|
||||||
|
Ms Sheila Wolanski/Mid-Island Home Support,MS SHEILA WOLANSKI/MID-ISLAND HOME SUPPORT
|
||||||
|
Naomi Anne Ecob,NAOMI ANNE ECOB
|
||||||
|
National Orthotic Centre/# 1703639,NATIONAL ORTHOTIC CENTRE/# 1703639
|
||||||
|
National Orthotic Centre/#1649481,NATIONAL ORTHOTIC CENTRE/#1649481
|
||||||
|
New Sudbury/Val Caron Family Vision Ctre,NEW SUDBURY/VAL CARON FAMILY VISION CTRE
|
||||||
|
Noelle Bethea,Noelle Bethea
|
||||||
|
North York General Hospital/Audiology,NORTH YORK GENERAL HOSPITAL/AUDIOLOGY
|
||||||
|
Not Just Backs! Chiropractic,NOT JUST BACKS! CHIROPRACTIC
|
||||||
|
Oh! Lunettes Par Sardi-Nicopoulos,OH! LUNETTES PAR SARDI-NICOPOULOS
|
||||||
|
Omod Kitchen/Guelph/Waterloo,OMOD KITCHEN/GUELPH/WATERLOO
|
||||||
|
Ontario Hearing Institute C/O Dorothy Bravo,ONTARIO HEARING INSTITUTE C/O DOROTHY BRAVO
|
||||||
|
Optical 20/20 of Whitby,OPTICAL 20/20 OF WHITBY
|
||||||
|
Optical 6/6,OPTICAL 6/6
|
||||||
|
Ortho Ml Inc./Respir-O-Max,ORTHO ML INC./RESPIR-O-MAX
|
||||||
|
Ossur Canada Inc C/OT44606,OSSUR CANADA INC C/OT44606
|
||||||
|
Ot Consulting/Treatment Services Ltd,OT CONSULTING/TREATMENT SERVICES LTD
|
||||||
|
Ottawa Eyelabs Inc C/O Vision Plus,OTTAWA EYELABS INC C/O VISION PLUS
|
||||||
|
Ottawa Health: Performance and Rehabilitation,OTTAWA HEALTH: PERFORMANCE AND REHABILITATION
|
||||||
|
Pearle Vision 9861 (Orchard Park S/C),PEARLE VISION 9861 (ORCHARD PARK S/C)
|
||||||
|
Pfahl's Drugs/Home Health Care,PFAHL'S DRUGS/HOME HEALTH CARE
|
||||||
|
Physio F/X Ltd,PHYSIO F/X LTD
|
||||||
|
Physiotherapy Works!,PHYSIOTHERAPY WORKS!
|
||||||
|
Pinellas County Ems D/B/A Sunstar,PINELLAS COUNTY EMS D/B/A SUNSTAR
|
||||||
|
Pmb/Emergency Medicine of in LLC,PMB/EMERGENCY MEDICINE OF IN LLC
|
||||||
|
Professional Hearing Clinic Inc/Connect Hearing,PROFESSIONAL HEARING CLINIC INC/CONNECT HEARING
|
||||||
|
Prothotics/Healthwest,PROTHOTICS/HEALTHWEST
|
||||||
|
Regents of The U of M U/M Health System,REGENTS OF THE U OF M U/M HEALTH SYSTEM
|
||||||
|
Regents of U/M - Medequip,REGENTS OF U/M - MEDEQUIP
|
||||||
|
Rehabilitation Network Canada - Guelph/Kitchener/Waterloo,REHABILITATION NETWORK CANADA - GUELPH/KITCHENER/WATERLOO
|
||||||
|
Rehabilitation Network Canada - Halton/Peel,REHABILITATION NETWORK CANADA - HALTON/PEEL
|
||||||
|
Rehabilitation Network Canada Inc - Halton/Peel,REHABILITATION NETWORK CANADA INC - HALTON/PEEL
|
||||||
|
Rehamed Inc O/A Humbertown Physiotherapy,REHAMED INC O/A HUMBERTOWN PHYSIOTHERAPY
|
||||||
|
Retire -at- Home Services/North York,RETIRE -AT- HOME SERVICES/NORTH YORK
|
||||||
|
Retire-at-Home 0SHAWA/Clarington,RETIRE-AT-HOME 0SHAWA/CLARINGTON
|
||||||
|
Richard Kievitz/Hear at Last,RICHARD KIEVITZ/HEAR AT LAST
|
||||||
|
Sam Ibraham/Canes Family Hlth Team,SAM IBRAHAM/CANES FAMILY HLTH TEAM
|
||||||
|
Sarah Ann Mary Pollock,SARAH ANN MARY POLLOCK
|
||||||
|
Sharp Healthcare Pfs/Icd Dept,SHARP HEALTHCARE PFS/ICD DEPT
|
||||||
|
Shelburne Medical Drugs O/A Caravaggio Ida,SHELBURNE MEDICAL DRUGS O/A CARAVAGGIO IDA
|
||||||
|
Silver Cross O/A 3004773,SILVER CROSS O/A 3004773
|
||||||
|
Six Nations Ltc/Hcc,SIX NATIONS LTC/HCC
|
||||||
|
Sole Science Inc/Co St Thomas,SOLE SCIENCE INC/CO ST THOMAS
|
||||||
|
Sonago Pharmacy Ltd C/O Main Drug Mart,SONAGO PHARMACY LTD C/O MAIN DRUG MART
|
||||||
|
Soul: The Wheelchair Studio,SOUL: THE WHEELCHAIR STUDIO
|
||||||
|
Spi Health & Safety Inc C/O Acctng,SPI HEALTH & SAFETY INC C/O ACCTNG
|
||||||
|
St Joseph Nuclear Med C/O St Josephs Hlth Ctr,ST JOSEPH NUCLEAR MED C/O ST JOSEPHS HLTH CTR
|
||||||
|
St. Meena Pharmacy Ltd O/A College Ctr Pharmacy,ST. MEENA PHARMACY LTD O/A COLLEGE CTR PHARMACY
|
||||||
|
Staples / Bd #235 Woodstock,STAPLES / BD #235 WOODSTOCK
|
||||||
|
Staples/Home Depot,STAPLES/HOME DEPOT
|
||||||
|
Sylvia Pudsey O/A The Renfrew Learning Centre,SYLVIA PUDSEY O/A THE RENFREW LEARNING CENTRE
|
||||||
|
Tarit Kumar Kanungo,Tarit Kumar Kanungo
|
||||||
|
The City of Winnipeg/Fire Paramedic Service,THE CITY OF WINNIPEG/FIRE PARAMEDIC SERVICE
|
||||||
|
The Hearing Loss Clinic Inc /Sabrina Rhodes,THE HEARING LOSS CLINIC INC /SABRINA RHODES
|
||||||
|
The Physio Clinic @ West Durham,THE PHYSIO CLINIC @ WEST DURHAM
|
||||||
|
The Rehabilitation Ctre Finance Dept/Accts Rec,THE REHABILITATION CTRE FINANCE DEPT/ACCTS REC
|
||||||
|
The Therapy Centre (C/O Dr Mary Cooke),THE THERAPY CENTRE (C/O DR MARY COOKE)
|
||||||
|
Town of Windham C/O Comstar Ambulance Billing Ser,TOWN OF WINDHAM C/O COMSTAR AMBULANCE BILLING SER
|
||||||
|
Tram Anh Thi Nguyen,TRAM ANH THI NGUYEN
|
||||||
|
Trillium Health Partners-Qhc Finance/Accts Rec-Cvh,TRILLIUM HEALTH PARTNERS-QHC FINANCE/ACCTS REC-CVH
|
||||||
|
Trimble Europe B V C/O T10271C,TRIMBLE EUROPE B V C/O T10271C
|
||||||
|
True North Imaging / 1582235 Ontario Ltd.,TRUE NORTH IMAGING / 1582235 ONTARIO LTD.
|
||||||
|
Tupley Optical Inc. C/O Aspen Eye Care,TUPLEY OPTICAL INC. C/O ASPEN EYE CARE
|
||||||
|
Uwo Staff/Faculty Family Practice Clinic,UWO STAFF/FACULTY FAMILY PRACTICE CLINIC
|
||||||
|
Visionworks / Eyes Inpsire,VISIONWORKS / EYES INPSIRE
|
||||||
|
Whole>Sum Massage,WHOLE>SUM Massage
|
||||||
|
William L. Trenwith / Paul J. Trenwith,WILLIAM L. TRENWITH / PAUL J. TRENWITH
|
||||||
|
Wilson Medical Centre/Evans,WILSON MEDICAL CENTRE/EVANS
|
||||||
|
Work Fitness Plus C/O Oakville Trafalgar Mh,WORK FITNESS PLUS C/O OAKVILLE TRAFALGAR MH
|
||||||
|
Ymca of Simcoe/Muskoka,YMCA OF SIMCOE/MUSKOKA
|
||||||
|
Young Drivers of Canada/ Maitland For Lincoln,YOUNG DRIVERS OF CANADA/ MAITLAND FOR LINCOLN
|
||||||
|
Zest! Rehabilitation Health & Wellness,ZEST! REHABILITATION HEALTH & WELLNESS
|
||||||
|
-Academy of Learning Owen Sound (Priv.),~ACADEMY OF LEARNING OWEN SOUND (PRIV.)
|
||||||
|
-Appletree Medical Centre,~APPLETREE MEDICAL CENTRE
|
||||||
|
-Brameast Family Physicians,~BRAMEAST FAMILY PHYSICIANS
|
||||||
|
-C. L. Consulting,~C. L. CONSULTING
|
||||||
|
-My Health Centre,~MY HEALTH CENTRE
|
||||||
|
-Upper Canada Hearing & Speech Centre,~UPPER CANADA HEARING & SPEECH CENTRE
|
||||||
|
-Whitby Clinic,~WHITBY CLINIC
|
||||||
|
Sport Physio West Island Inc./Actio,Sport Physio West Island Inc./Actio
|
||||||
|
Voir...Être Vu! Opticiens,VOIR...ÊTRE VU! OPTICIENS
|
||||||
|
Vpi Inc. - Halton/Peel,VPI INC. - HALTON/PEEL
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.address;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.helpers.AddressHelper;
|
||||||
|
import org.hl7.fhir.r4.model.Address;
|
||||||
|
import org.hl7.fhir.r4.model.HumanName;
|
||||||
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class AddressHelperTest {
|
||||||
|
|
||||||
|
private static FhirContext ourContext = FhirContext.forR4();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalid() {
|
||||||
|
HumanName name = new HumanName();
|
||||||
|
name.setFamily("Test");
|
||||||
|
|
||||||
|
final AddressHelper helper = new AddressHelper(null, name);
|
||||||
|
assertThrows(IllegalStateException.class, () -> {
|
||||||
|
helper.getCountry();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
new AddressHelper(null, new StringType("this will blow up"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCountry() {
|
||||||
|
Address a = new Address();
|
||||||
|
a.setCountry("Test");
|
||||||
|
|
||||||
|
AddressHelper helper = new AddressHelper(null, a);
|
||||||
|
assertEquals("Test", helper.getCountry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getParts() {
|
||||||
|
Address a = new Address();
|
||||||
|
a.setCity("Hammer");
|
||||||
|
|
||||||
|
AddressHelper helper = new AddressHelper(null, a);
|
||||||
|
helper.setDelimiter("; ");
|
||||||
|
assertEquals("Hammer", helper.getParts());
|
||||||
|
|
||||||
|
a.addLine("Street");
|
||||||
|
a.setPostalCode("L9C6L6");
|
||||||
|
assertEquals("Hammer; L9C6L6", helper.getParts());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getLine() {
|
||||||
|
Address a = new Address();
|
||||||
|
a.addLine("Unit 10");
|
||||||
|
a.setCity("Hammer");
|
||||||
|
|
||||||
|
AddressHelper helper = new AddressHelper(null, a);
|
||||||
|
assertEquals("Unit 10", helper.getLine());
|
||||||
|
|
||||||
|
a.addLine("100 Main St.");
|
||||||
|
assertEquals("Unit 10, 100 Main St.", helper.getLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetFields() {
|
||||||
|
Address a = new Address();
|
||||||
|
|
||||||
|
AddressHelper helper = new AddressHelper(ourContext, a);
|
||||||
|
helper.addLine("Line 1").addLine("Line 2");
|
||||||
|
helper.setCity("Hammer");
|
||||||
|
helper.setState("State");
|
||||||
|
helper.setCountry("Country");
|
||||||
|
helper.setText("Some Text Too");
|
||||||
|
assertEquals("Some Text Too, Line 1, Line 2, Hammer, State, Country", helper.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.s13n.interceptors;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.s13n.StandardizingInterceptor;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.r4.model.ContactPoint;
|
||||||
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.hl7.fhir.r4.model.Person;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import java.lang.reflect.Array;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.rest.server.interceptor.s13n.StandardizingInterceptor.STANDARDIZATION_DISABLED_HEADER;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
class StandardizingInterceptorTest {
|
||||||
|
|
||||||
|
private static final String CONFIG =
|
||||||
|
"{\n" +
|
||||||
|
"\t\"Person\" : {\n" +
|
||||||
|
"\t\t\"Person.name.family\" : \"NAME_FAMILY\",\n" +
|
||||||
|
"\t\t\"Person.name.given\" : \"NAME_GIVEN\",\n" +
|
||||||
|
"\t\t\"Person.telecom.where(system='phone').value\" : \"PHONE\"\n" +
|
||||||
|
"\t\t},\n" +
|
||||||
|
"\t\"Patient\" : {\n" +
|
||||||
|
"\t\t\"name.given\" : \"NAME_GIVEN\",\n" +
|
||||||
|
"\t\t\"telecom.where(system='phone').value\" : \"PHONE\"\n" +
|
||||||
|
"\t\t}\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
private static final String BAD_CONFIG = "{ \"Person\" : { \"Person.name.family\" : \"org.nonexistent.Standardizer\"}}";
|
||||||
|
|
||||||
|
private static FhirContext ourCtx = FhirContext.forR4();
|
||||||
|
|
||||||
|
private RequestDetails myRequestDetails;
|
||||||
|
|
||||||
|
private StandardizingInterceptor myInterceptor = new StandardizingInterceptor();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void init() throws Exception {
|
||||||
|
myInterceptor = new StandardizingInterceptor(new ObjectMapper().readValue(CONFIG, Map.class));
|
||||||
|
|
||||||
|
myRequestDetails = mock(RequestDetails.class);
|
||||||
|
when(myRequestDetails.getFhirContext()).thenReturn(ourCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNameStandardization() throws Exception {
|
||||||
|
Person p = new Person();
|
||||||
|
p.addName().setFamily("macdouglas1").addGiven("\nJoHn");
|
||||||
|
p.addName().setFamily("o'BrIaN").addGiven("jIM\t");
|
||||||
|
|
||||||
|
myInterceptor.resourcePreUpdate(myRequestDetails, null, p);
|
||||||
|
|
||||||
|
assertEquals("John MacDouglas1", p.getName().get(0).getNameAsSingleString());
|
||||||
|
assertEquals("Jim O'Brian", p.getName().get(1).getNameAsSingleString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNullsWork() {
|
||||||
|
try {
|
||||||
|
myInterceptor.resourcePreCreate(myRequestDetails, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBadConfig() throws Exception {
|
||||||
|
myInterceptor = new StandardizingInterceptor(new ObjectMapper().readValue(BAD_CONFIG, Map.class));
|
||||||
|
|
||||||
|
try {
|
||||||
|
myInterceptor.resourcePreCreate(myRequestDetails, new Person());
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDisablingValidationViaHeader() {
|
||||||
|
when(myRequestDetails.getHeaders(eq(STANDARDIZATION_DISABLED_HEADER))).thenReturn(Arrays.asList(new String[]{"True"}));
|
||||||
|
|
||||||
|
Person p = new Person();
|
||||||
|
p.addName().setFamily("non'normalized").addGiven("name");
|
||||||
|
|
||||||
|
myInterceptor.resourcePreUpdate(myRequestDetails, null, p);
|
||||||
|
|
||||||
|
assertEquals("name non'normalized", p.getName().get(0).getNameAsSingleString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTelecomStandardization() throws Exception {
|
||||||
|
Person p = new Person();
|
||||||
|
p.addTelecom().setValue(" Email@email.com").setSystem(ContactPoint.ContactPointSystem.EMAIL);
|
||||||
|
p.addTelecom().setValue("1234567890").setSystem(ContactPoint.ContactPointSystem.PHONE);
|
||||||
|
|
||||||
|
myInterceptor.resourcePreUpdate(myRequestDetails, null, p);
|
||||||
|
|
||||||
|
assertEquals(" Email@email.com", p.getTelecom().get(0).getValue(), "Expected email to remain the same");
|
||||||
|
assertEquals("123-456-7890", p.getTelecom().get(1).getValue());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import org.checkerframework.checker.units.qual.A;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.r4.model.Address;
|
||||||
|
import org.hl7.fhir.r4.model.Person;
|
||||||
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.rest.server.interceptor.s13n.StandardizingInterceptor.STANDARDIZATION_DISABLED_HEADER;
|
||||||
|
import static ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidatingInterceptor.ADDRESS_VALIDATION_DISABLED_HEADER;
|
||||||
|
import static ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidatingInterceptor.PROPERTY_VALIDATOR_CLASS;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class AddressValidatingInterceptorTest {
|
||||||
|
|
||||||
|
private static FhirContext ourCtx = FhirContext.forR4();
|
||||||
|
|
||||||
|
private AddressValidatingInterceptor myInterceptor;
|
||||||
|
|
||||||
|
private IAddressValidator myValidator;
|
||||||
|
|
||||||
|
private RequestDetails myRequestDetails;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void start() throws Exception {
|
||||||
|
AddressValidatingInterceptor interceptor = new AddressValidatingInterceptor(new Properties());
|
||||||
|
assertNull(interceptor.getAddressValidator());
|
||||||
|
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.setProperty(PROPERTY_VALIDATOR_CLASS, "RandomService");
|
||||||
|
try {
|
||||||
|
new AddressValidatingInterceptor(props);
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
props.setProperty(PROPERTY_VALIDATOR_CLASS, TestAddressValidator.class.getName());
|
||||||
|
interceptor = new AddressValidatingInterceptor(props);
|
||||||
|
assertNotNull(interceptor.getAddressValidator());
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
myValidator = mock(IAddressValidator.class);
|
||||||
|
when(myValidator.isValid(any(), any())).thenReturn(mock(AddressValidationResult.class));
|
||||||
|
|
||||||
|
myRequestDetails = mock(RequestDetails.class);
|
||||||
|
when(myRequestDetails.getFhirContext()).thenReturn(ourCtx);
|
||||||
|
|
||||||
|
Properties properties = getProperties();
|
||||||
|
myInterceptor = new AddressValidatingInterceptor(properties);
|
||||||
|
myInterceptor.setAddressValidator(myValidator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Properties getProperties() {
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.setProperty(PROPERTY_VALIDATOR_CLASS, TestAddressValidator.class.getName());
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDisablingValidationViaHeader() {
|
||||||
|
when(myRequestDetails.getHeaders(eq(ADDRESS_VALIDATION_DISABLED_HEADER))).thenReturn(Arrays.asList(new String[]{"True"}));
|
||||||
|
|
||||||
|
Person p = new Person();
|
||||||
|
AddressValidatingInterceptor spy = Mockito.spy(myInterceptor);
|
||||||
|
spy.resourcePreCreate(myRequestDetails, p);
|
||||||
|
|
||||||
|
Mockito.verify(spy, times(0)).validateAddress(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationServiceError() {
|
||||||
|
myValidator = mock(IAddressValidator.class);
|
||||||
|
when(myValidator.isValid(any(), any())).thenThrow(new RuntimeException());
|
||||||
|
myInterceptor.setAddressValidator(myValidator);
|
||||||
|
|
||||||
|
Address address = new Address();
|
||||||
|
myInterceptor.validateAddress(address, ourCtx);
|
||||||
|
assertValidated(address, "not-validated");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validate() {
|
||||||
|
Address address = new Address();
|
||||||
|
address.addLine("Line");
|
||||||
|
address.setCity("City");
|
||||||
|
|
||||||
|
myInterceptor.validateAddress(address, ourCtx);
|
||||||
|
assertValidated(address, "invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertValidated(Address theAddress, String theValidationResult) {
|
||||||
|
assertTrue(theAddress.hasExtension());
|
||||||
|
assertEquals(1, theAddress.getExtension().size());
|
||||||
|
assertEquals(IAddressValidator.ADDRESS_VALIDATION_EXTENSION_URL, theAddress.getExtensionFirstRep().getUrl());
|
||||||
|
assertEquals(theValidationResult, theAddress.getExtensionFirstRep().getValueAsPrimitive().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validateOnCreate() {
|
||||||
|
Address address = new Address();
|
||||||
|
address.addLine("Line");
|
||||||
|
address.setCity("City");
|
||||||
|
|
||||||
|
Person person = new Person();
|
||||||
|
person.addAddress(address);
|
||||||
|
|
||||||
|
myInterceptor.resourcePreCreate(myRequestDetails, person);
|
||||||
|
|
||||||
|
assertValidated(person.getAddressFirstRep(), "invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validateOnUpdate() {
|
||||||
|
Address address = new Address();
|
||||||
|
address.addLine("Line");
|
||||||
|
address.setCity("City");
|
||||||
|
address.addExtension(IAddressValidator.ADDRESS_VALIDATION_EXTENSION_URL, new StringType("..."));
|
||||||
|
|
||||||
|
Address address2 = new Address();
|
||||||
|
address2.addLine("Line 2");
|
||||||
|
address2.setCity("City 2");
|
||||||
|
|
||||||
|
Person person = new Person();
|
||||||
|
person.addAddress(address);
|
||||||
|
person.addAddress(address2);
|
||||||
|
|
||||||
|
myInterceptor.resourcePreUpdate(myRequestDetails, null, person);
|
||||||
|
|
||||||
|
verify(myValidator, times(1)).isValid(any(), any());
|
||||||
|
assertValidated(person.getAddress().get(0), "...");
|
||||||
|
assertValidated(person.getAddress().get(1), "invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TestAddressValidator implements IAddressValidator {
|
||||||
|
@Override
|
||||||
|
public AddressValidationResult isValid(IBase theAddress, FhirContext theFhirContext) throws AddressValidationException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address.impl;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationResult;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.r4.model.Address;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
class BaseRestfulValidatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHappyPath() throws Exception {
|
||||||
|
ResponseEntity responseEntity = mock(ResponseEntity.class);
|
||||||
|
when(responseEntity.getStatusCode()).thenReturn(HttpStatus.OK);
|
||||||
|
when(responseEntity.getBody()).thenReturn("{}");
|
||||||
|
|
||||||
|
TestRestfulValidator val = spy(new TestRestfulValidator(responseEntity));
|
||||||
|
assertNotNull(val.isValid(new Address(), FhirContext.forR4()));
|
||||||
|
|
||||||
|
verify(val, times(1)).getResponseEntity(any(IBase.class), any(FhirContext.class));
|
||||||
|
verify(val, times(1)).getValidationResult(any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsValid() throws Exception {
|
||||||
|
ResponseEntity responseEntity = mock(ResponseEntity.class);
|
||||||
|
when(responseEntity.getStatusCode()).thenReturn(HttpStatus.REQUEST_TIMEOUT);
|
||||||
|
|
||||||
|
TestRestfulValidator val = new TestRestfulValidator(responseEntity);
|
||||||
|
try {
|
||||||
|
assertNotNull(val.isValid(new Address(), FhirContext.forR4()));
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestRestfulValidator extends BaseRestfulValidator {
|
||||||
|
ResponseEntity<String> myResponseEntity;
|
||||||
|
|
||||||
|
public TestRestfulValidator(ResponseEntity<String> theResponseEntity) {
|
||||||
|
super(null);
|
||||||
|
myResponseEntity = theResponseEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AddressValidationResult getValidationResult(AddressValidationResult theResult, JsonNode response, FhirContext theFhirContext) throws Exception {
|
||||||
|
return new AddressValidationResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<String> getResponseEntity(IBase theAddress, FhirContext theFhirContext) throws Exception {
|
||||||
|
return myResponseEntity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address.impl;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationException;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationResult;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.hl7.fhir.r4.model.Address;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
class LoquateAddressValidatorTest {
|
||||||
|
|
||||||
|
private static final String REQUEST = "{\n" +
|
||||||
|
" \"Key\" : \"MY_KEY\",\n" +
|
||||||
|
" \"Geocode\" : false,\n" +
|
||||||
|
" \"Addresses\" : [ {\n" +
|
||||||
|
" \"Address1\" : \"Line 1\",\n" +
|
||||||
|
" \"Address2\" : \"Line 2\",\n" +
|
||||||
|
" \"Locality\" : \"City\",\n" +
|
||||||
|
" \"PostalCode\" : \"POSTAL\",\n" +
|
||||||
|
" \"Country\" : \"Country\"\n" +
|
||||||
|
" } ]\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
private static final String RESPONSE_INVALID = "[\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"Input\": {\n" +
|
||||||
|
" \"Address\": \"\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
"]";
|
||||||
|
|
||||||
|
private static final String RESPONSE_INVALID_ADDRESS = "[\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"Input\": {\n" +
|
||||||
|
" \"Address\": \"\"\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"Matches\": [\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"AQI\": \"C\",\n" +
|
||||||
|
" \"Address\": \"\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
" ]\n" +
|
||||||
|
" }\n" +
|
||||||
|
"]";
|
||||||
|
|
||||||
|
private static final String RESPONSE_VALID_ADDRESS = "[\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"Input\": {\n" +
|
||||||
|
" \"Address\": \"\"\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"Matches\": [\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"AQI\": \"A\",\n" +
|
||||||
|
" \"Address\": \"My Valid Address\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
" ]\n" +
|
||||||
|
" }\n" +
|
||||||
|
"]";
|
||||||
|
|
||||||
|
private static final String RESPONSE_INVALID_KEY = "{\n" +
|
||||||
|
" \"Number\": 2,\n" +
|
||||||
|
" \"Description\": \"Unknown key\",\n" +
|
||||||
|
" \"Cause\": \"The key you are using to access the service was not found.\",\n" +
|
||||||
|
" \"Resolution\": \"Please check that the key is correct. It should be in the form AA11-AA11-AA11-AA11.\"\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
private static FhirContext ourCtx = FhirContext.forR4();
|
||||||
|
|
||||||
|
private LoquateAddressValidator myValidator;
|
||||||
|
|
||||||
|
private Properties myProperties;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void initValidator() {
|
||||||
|
myProperties = new Properties();
|
||||||
|
myProperties.setProperty(LoquateAddressValidator.PROPERTY_SERVICE_KEY, "MY_KEY");
|
||||||
|
myValidator = new LoquateAddressValidator(myProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidInit() {
|
||||||
|
try {
|
||||||
|
new LoquateAddressValidator(new Properties());
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidAddressValidationResponse() throws Exception {
|
||||||
|
try {
|
||||||
|
AddressValidationResult res = myValidator.getValidationResult(new AddressValidationResult(),
|
||||||
|
new ObjectMapper().readTree(RESPONSE_INVALID), ourCtx);
|
||||||
|
fail();
|
||||||
|
} catch (AddressValidationException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequestBody() {
|
||||||
|
try {
|
||||||
|
assertEquals(clear(REQUEST), clear(myValidator.getRequestBody(ourCtx, getAddress())));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String clear(String theString) {
|
||||||
|
theString = theString.replaceAll("\n", "");
|
||||||
|
theString = theString.replaceAll("\r", "");
|
||||||
|
return theString.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testServiceCalled() {
|
||||||
|
Address address = getAddress();
|
||||||
|
|
||||||
|
final RestTemplate template = mock(RestTemplate.class);
|
||||||
|
|
||||||
|
LoquateAddressValidator val = new LoquateAddressValidator(myProperties) {
|
||||||
|
@Override
|
||||||
|
protected RestTemplate newTemplate() {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
val.getResponseEntity(address, ourCtx);
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(template, times(1)).postForEntity(any(String.class), any(HttpEntity.class), eq(String.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Address getAddress() {
|
||||||
|
Address address = new Address();
|
||||||
|
address.addLine("Line 1").addLine("Line 2").setCity("City").setPostalCode("POSTAL").setCountry("Country");
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccessfulResponses() throws Exception {
|
||||||
|
AddressValidationResult res = myValidator.getValidationResult(new AddressValidationResult(),
|
||||||
|
new ObjectMapper().readTree(RESPONSE_INVALID_ADDRESS), ourCtx);
|
||||||
|
assertFalse(res.isValid());
|
||||||
|
|
||||||
|
res = myValidator.getValidationResult(new AddressValidationResult(),
|
||||||
|
new ObjectMapper().readTree(RESPONSE_VALID_ADDRESS), ourCtx);
|
||||||
|
assertTrue(res.isValid());
|
||||||
|
assertEquals("My Valid Address", res.getValidatedAddressString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testErrorResponses() throws Exception {
|
||||||
|
assertThrows(AddressValidationException.class, () -> {
|
||||||
|
myValidator.getValidationResult(new AddressValidationResult(),
|
||||||
|
new ObjectMapper().readTree(RESPONSE_INVALID_KEY), ourCtx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.address.impl;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationException;
|
||||||
|
import ca.uhn.fhir.rest.server.interceptor.validation.address.AddressValidationResult;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.hl7.fhir.r4.model.Address;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
class MelissaAddressValidatorTest {
|
||||||
|
|
||||||
|
private static final String RESPONSE_INVALID_ADDRESS = "{\n" +
|
||||||
|
" \"Version\": \"3.0.1.160\",\n" +
|
||||||
|
" \"TransmissionReference\": \"1\",\n" +
|
||||||
|
" \"TransmissionResults\": \"\",\n" +
|
||||||
|
" \"TotalRecords\": \"1\",\n" +
|
||||||
|
" \"Records\": [\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"RecordID\": \"1\",\n" +
|
||||||
|
" \"Results\": \"AC01,AC12,AE02,AV12,GE02\",\n" +
|
||||||
|
" \"FormattedAddress\": \"100 Main Street\",\n" +
|
||||||
|
" \"Organization\": \"\",\n" +
|
||||||
|
" \"AddressLine1\": \"100 Main Street\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
" ]\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
private static final String RESPONSE_VALID_ADDRESS = "{\n" +
|
||||||
|
" \"Version\": \"3.0.1.160\",\n" +
|
||||||
|
" \"TransmissionReference\": \"1\",\n" +
|
||||||
|
" \"TransmissionResults\": \"\",\n" +
|
||||||
|
" \"TotalRecords\": \"1\",\n" +
|
||||||
|
" \"Records\": [\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"RecordID\": \"1\",\n" +
|
||||||
|
" \"Results\": \"AC01,AV24,GS05\",\n" +
|
||||||
|
" \"FormattedAddress\": \"100 Main St W;Hamilton ON L8P 1H6\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
" ]\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
private static final String RESPONSE_INVALID_KEY = "{\n" +
|
||||||
|
" \"Version\": \"3.0.1.160\",\n" +
|
||||||
|
" \"TransmissionReference\": \"1\",\n" +
|
||||||
|
" \"TransmissionResults\": \"GE05\",\n" +
|
||||||
|
" \"TotalRecords\": \"0\"\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
private static FhirContext ourContext = FhirContext.forR4();
|
||||||
|
|
||||||
|
private MelissaAddressValidator myValidator;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void init() {
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.setProperty(MelissaAddressValidator.PROPERTY_SERVICE_KEY, "MY_KEY");
|
||||||
|
myValidator = new MelissaAddressValidator(props);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequestBody() {
|
||||||
|
Map<String, String> params = myValidator.getRequestParams(getAddress());
|
||||||
|
|
||||||
|
assertEquals("Line 1, Line 2", params.get("a1"));
|
||||||
|
assertEquals("City, POSTAL", params.get("a2"));
|
||||||
|
assertEquals("Country", params.get("ctry"));
|
||||||
|
assertEquals("MY_KEY", params.get("id"));
|
||||||
|
assertEquals("json", params.get("format"));
|
||||||
|
assertTrue(params.containsKey("t"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testServiceCalled() {
|
||||||
|
Address address = getAddress();
|
||||||
|
|
||||||
|
final RestTemplate template = mock(RestTemplate.class);
|
||||||
|
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.setProperty(BaseRestfulValidator.PROPERTY_SERVICE_KEY, "MY_KEY");
|
||||||
|
MelissaAddressValidator val = new MelissaAddressValidator(props) {
|
||||||
|
@Override
|
||||||
|
protected RestTemplate newTemplate() {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
val.getResponseEntity(address, ourContext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(template, times(1)).getForEntity(any(String.class), eq(String.class), any(Map.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Address getAddress() {
|
||||||
|
Address address = new Address();
|
||||||
|
address.addLine("Line 1").addLine("Line 2").setCity("City").setPostalCode("POSTAL").setCountry("Country");
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccessfulResponses() throws Exception {
|
||||||
|
AddressValidationResult res = myValidator.getValidationResult(new AddressValidationResult(),
|
||||||
|
new ObjectMapper().readTree(RESPONSE_INVALID_ADDRESS), ourContext);
|
||||||
|
assertFalse(res.isValid());
|
||||||
|
|
||||||
|
res = myValidator.getValidationResult(new AddressValidationResult(),
|
||||||
|
new ObjectMapper().readTree(RESPONSE_VALID_ADDRESS), ourContext);
|
||||||
|
assertTrue(res.isValid());
|
||||||
|
assertEquals("100 Main St W;Hamilton ON L8P 1H6", res.getValidatedAddressString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testErrorResponses() throws Exception {
|
||||||
|
assertThrows(AddressValidationException.class, () -> {
|
||||||
|
myValidator.getValidationResult(new AddressValidationResult(),
|
||||||
|
new ObjectMapper().readTree(RESPONSE_INVALID_KEY), ourContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package ca.uhn.fhir.rest.server.interceptor.validation.fields;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.hl7.fhir.r4.model.ContactPoint;
|
||||||
|
import org.hl7.fhir.r4.model.Person;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.rest.server.interceptor.s13n.StandardizingInterceptor.STANDARDIZATION_DISABLED_HEADER;
|
||||||
|
import static ca.uhn.fhir.rest.server.interceptor.validation.fields.FieldValidatingInterceptor.VALIDATION_DISABLED_HEADER;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class FieldValidatingInterceptorTest {
|
||||||
|
|
||||||
|
private FhirContext myFhirContext = FhirContext.forR4();
|
||||||
|
private FieldValidatingInterceptor myInterceptor = new FieldValidatingInterceptor();
|
||||||
|
|
||||||
|
public RequestDetails newRequestDetails() {
|
||||||
|
RequestDetails requestDetails = mock(RequestDetails.class);
|
||||||
|
when(requestDetails.getFhirContext()).thenReturn(myFhirContext);
|
||||||
|
return requestDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void init() throws Exception {
|
||||||
|
myInterceptor = new FieldValidatingInterceptor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDisablingValidationViaHeader() {
|
||||||
|
RequestDetails request = newRequestDetails();
|
||||||
|
when(request.getHeaders(eq(VALIDATION_DISABLED_HEADER))).thenReturn(Arrays.asList(new String[]{"True"}));
|
||||||
|
|
||||||
|
Person person = new Person();
|
||||||
|
person.addTelecom().setSystem(ContactPoint.ContactPointSystem.EMAIL).setValue("EMAIL");
|
||||||
|
|
||||||
|
myInterceptor.handleRequest(request, person);
|
||||||
|
assertEquals("EMAIL", person.getTelecom().get(0).getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmailValidation() {
|
||||||
|
Person person = new Person();
|
||||||
|
person.addTelecom().setSystem(ContactPoint.ContactPointSystem.EMAIL).setValue("email@email.com");
|
||||||
|
|
||||||
|
try {
|
||||||
|
myInterceptor.handleRequest(newRequestDetails(), person);
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidEmailValidation() {
|
||||||
|
Person person = new Person();
|
||||||
|
person.addTelecom().setSystem(ContactPoint.ContactPointSystem.EMAIL).setValue("@garbage");
|
||||||
|
|
||||||
|
try {
|
||||||
|
myInterceptor.handleRequest(newRequestDetails(), person);
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomInvalidValidation() {
|
||||||
|
myInterceptor.getConfig().put("telecom.where(system='phone').value", "ClassThatDoesntExist");
|
||||||
|
try {
|
||||||
|
myInterceptor.handleRequest(newRequestDetails(), new Person());
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomValidation() {
|
||||||
|
myInterceptor.getConfig().put("telecom.where(system='phone').value", EmptyValidator.class.getName());
|
||||||
|
|
||||||
|
Person person = new Person();
|
||||||
|
person.addTelecom().setSystem(ContactPoint.ContactPointSystem.EMAIL).setValue("email@email.com");
|
||||||
|
|
||||||
|
try {
|
||||||
|
myInterceptor.handleRequest(newRequestDetails(), person);
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
person.addTelecom().setSystem(ContactPoint.ContactPointSystem.PHONE).setValue("123456");
|
||||||
|
try {
|
||||||
|
myInterceptor.handleRequest(newRequestDetails(), person);
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
person = new Person();
|
||||||
|
person.addTelecom().setSystem(ContactPoint.ContactPointSystem.PHONE).setValue(" ");
|
||||||
|
try {
|
||||||
|
myInterceptor.handleRequest(newRequestDetails(), person);
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EmptyValidator implements IValidator {
|
||||||
|
@Override
|
||||||
|
public boolean isValid(String theString) {
|
||||||
|
return !StringUtils.isBlank(theString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseDatatype;
|
||||||
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ExtensionUtilTest {
|
||||||
|
|
||||||
|
private static final String EXT_URL = "http://magic.com/extensions";
|
||||||
|
|
||||||
|
private static FhirContext ourFhirContext = FhirContext.forR4();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExtensionsWork() {
|
||||||
|
Patient p1 = new Patient();
|
||||||
|
assertFalse(ExtensionUtil.hasExtension(p1, EXT_URL));
|
||||||
|
ExtensionUtil.setExtension(ourFhirContext, p1, EXT_URL, "value");
|
||||||
|
assertTrue(ExtensionUtil.hasExtension(p1, EXT_URL));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExtensionTypesWork() {
|
||||||
|
Patient p1 = new Patient();
|
||||||
|
assertFalse(ExtensionUtil.hasExtension(p1, EXT_URL));
|
||||||
|
ExtensionUtil.setExtension(ourFhirContext, p1, EXT_URL, "integer", "1");
|
||||||
|
|
||||||
|
assertTrue(ExtensionUtil.hasExtension(p1, EXT_URL));
|
||||||
|
assertEquals(1, ExtensionUtil.getExtensions(p1, EXT_URL).size());
|
||||||
|
|
||||||
|
IBaseDatatype ext = ExtensionUtil.getExtension(p1, EXT_URL).getValue();
|
||||||
|
assertEquals("1", ext.toString());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import org.hl7.fhir.r4.model.Address;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
public class PropertyModifyingHelperTest {
|
||||||
|
|
||||||
|
private static FhirContext ourContext = FhirContext.forR4();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetAndGet() {
|
||||||
|
Address address = new Address();
|
||||||
|
|
||||||
|
PropertyModifyingHelper helper = new PropertyModifyingHelper(ourContext, address);
|
||||||
|
helper.set("line", "line1");
|
||||||
|
helper.set("line", "line2");
|
||||||
|
helper.set("city", "city");
|
||||||
|
|
||||||
|
address = (Address) helper.getBase();
|
||||||
|
|
||||||
|
assertEquals(2, address.getLine().size());
|
||||||
|
assertEquals("city", address.getCity());
|
||||||
|
assertNull(address.getCountry());
|
||||||
|
|
||||||
|
helper.setDelimiter(";");
|
||||||
|
assertEquals("line1;line2;city", helper.getFields("line", "city"));
|
||||||
|
List<String> lines = helper.getMultiple("line");
|
||||||
|
assertEquals("[line1, line2]", lines.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
|
import org.hl7.fhir.r4.model.DateTimeType;
|
||||||
|
import org.hl7.fhir.r4.model.DateType;
|
||||||
|
import org.hl7.fhir.r4.model.Enumerations;
|
||||||
|
import org.hl7.fhir.r4.model.Extension;
|
||||||
|
import org.hl7.fhir.r4.model.Identifier;
|
||||||
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class TerserUtilTest {
|
||||||
|
|
||||||
|
private FhirContext ourFhirContext = FhirContext.forR4();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCloneEidIntoResource() {
|
||||||
|
Identifier identifier = new Identifier().setSystem("http://org.com/sys").setValue("123");
|
||||||
|
|
||||||
|
Patient p1 = new Patient();
|
||||||
|
p1.addIdentifier(identifier);
|
||||||
|
|
||||||
|
Patient p2 = new Patient();
|
||||||
|
RuntimeResourceDefinition definition = ourFhirContext.getResourceDefinition(p1);
|
||||||
|
TerserUtil.cloneEidIntoResource(ourFhirContext, definition.getChildByName("identifier"), identifier, p2);
|
||||||
|
|
||||||
|
assertEquals(1, p2.getIdentifier().size());
|
||||||
|
assertEquals(p1.getIdentifier().get(0).getSystem(), p2.getIdentifier().get(0).getSystem());
|
||||||
|
assertEquals(p1.getIdentifier().get(0).getValue(), p2.getIdentifier().get(0).getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCloneEidIntoResourceViaHelper() {
|
||||||
|
TerserUtilHelper p1Helper = TerserUtilHelper.newHelper(ourFhirContext, "Patient");
|
||||||
|
p1Helper.setField("identifier.system", "http://org.com/sys");
|
||||||
|
p1Helper.setField("identifier.value", "123");
|
||||||
|
|
||||||
|
Patient p1 = p1Helper.getResource();
|
||||||
|
assertEquals(1, p1.getIdentifier().size());
|
||||||
|
|
||||||
|
TerserUtilHelper p2Helper = TerserUtilHelper.newHelper(ourFhirContext, "Patient");
|
||||||
|
RuntimeResourceDefinition definition = p1Helper.getResourceDefinition();
|
||||||
|
|
||||||
|
TerserUtil.cloneEidIntoResource(ourFhirContext, definition.getChildByName("identifier"),
|
||||||
|
p1.getIdentifier().get(0), p2Helper.getResource());
|
||||||
|
|
||||||
|
assertEquals(1, p2Helper.getFieldValues("identifier").size());
|
||||||
|
|
||||||
|
Identifier id1 = (Identifier) p1Helper.getFieldValues("identifier").get(0);
|
||||||
|
Identifier id2 = (Identifier) p2Helper.getFieldValues("identifier").get(0);
|
||||||
|
assertTrue(id1.equalsDeep(id2));
|
||||||
|
assertFalse(id1.equals(id2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetFieldsViaHelper() {
|
||||||
|
TerserUtilHelper p1Helper = TerserUtilHelper.newHelper(ourFhirContext, "Patient");
|
||||||
|
p1Helper.setField("active", "boolean", "true");
|
||||||
|
p1Helper.setField("birthDate", "date", "1999-01-01");
|
||||||
|
p1Helper.setField("gender", "code", "male");
|
||||||
|
|
||||||
|
Patient p = p1Helper.getResource();
|
||||||
|
assertTrue(p.getActive());
|
||||||
|
assertEquals(Enumerations.AdministrativeGender.MALE, p.getGender());
|
||||||
|
|
||||||
|
DateType check = TerserUtil.newElement(ourFhirContext, "date", "1999-01-01");
|
||||||
|
assertEquals(check.getValue(), p.getBirthDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFieldExists() {
|
||||||
|
assertTrue(TerserUtil.fieldExists(ourFhirContext, "identifier", TerserUtil.newResource(ourFhirContext, "Patient")));
|
||||||
|
assertFalse(TerserUtil.fieldExists(ourFhirContext, "randomFieldName", TerserUtil.newResource(ourFhirContext, "Patient")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCloneFields() {
|
||||||
|
Patient p1 = new Patient();
|
||||||
|
p1.addName().addGiven("Sigizmund");
|
||||||
|
|
||||||
|
Patient p2 = new Patient();
|
||||||
|
|
||||||
|
TerserUtil.mergeFieldsExceptIdAndMeta(ourFhirContext, p1, p2);
|
||||||
|
|
||||||
|
assertTrue(p2.getIdentifier().isEmpty());
|
||||||
|
|
||||||
|
assertNull(p2.getId());
|
||||||
|
assertEquals(1, p2.getName().size());
|
||||||
|
assertEquals(p1.getName().get(0).getNameAsSingleString(), p2.getName().get(0).getNameAsSingleString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCloneWithNonPrimitves() {
|
||||||
|
Patient p1 = new Patient();
|
||||||
|
Patient p2 = new Patient();
|
||||||
|
|
||||||
|
p1.addName().addGiven("Joe");
|
||||||
|
p1.getNameFirstRep().addGiven("George");
|
||||||
|
assertThat(p1.getName(), hasSize(1));
|
||||||
|
assertThat(p1.getName().get(0).getGiven(), hasSize(2));
|
||||||
|
|
||||||
|
p2.addName().addGiven("Jeff");
|
||||||
|
p2.getNameFirstRep().addGiven("George");
|
||||||
|
assertThat(p2.getName(), hasSize(1));
|
||||||
|
assertThat(p2.getName().get(0).getGiven(), hasSize(2));
|
||||||
|
|
||||||
|
TerserUtil.mergeAllFields(ourFhirContext, p1, p2);
|
||||||
|
assertThat(p2.getName(), hasSize(2));
|
||||||
|
assertThat(p2.getName().get(0).getGiven(), hasSize(2));
|
||||||
|
assertThat(p2.getName().get(1).getGiven(), hasSize(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMergeForAddressWithExtensions() {
|
||||||
|
Extension ext = new Extension();
|
||||||
|
ext.setUrl("http://hapifhir.io/extensions/address#create-timestamp");
|
||||||
|
ext.setValue(new DateTimeType("2021-01-02T11:13:15"));
|
||||||
|
|
||||||
|
Patient p1 = new Patient();
|
||||||
|
p1.addAddress()
|
||||||
|
.addLine("10 Main Street")
|
||||||
|
.setCity("Hamilton")
|
||||||
|
.setState("ON")
|
||||||
|
.setPostalCode("Z0Z0Z0")
|
||||||
|
.setCountry("Canada")
|
||||||
|
.addExtension(ext);
|
||||||
|
|
||||||
|
Patient p2 = new Patient();
|
||||||
|
p2.addAddress().addLine("10 Lenin Street").setCity("Severodvinsk").setCountry("Russia");
|
||||||
|
|
||||||
|
TerserUtil.mergeField(ourFhirContext, "address", p1, p2);
|
||||||
|
|
||||||
|
assertEquals(2, p2.getAddress().size());
|
||||||
|
assertEquals("[10 Lenin Street]", p2.getAddress().get(0).getLine().toString());
|
||||||
|
assertEquals("[10 Main Street]", p2.getAddress().get(1).getLine().toString());
|
||||||
|
assertTrue(p2.getAddress().get(1).hasExtension());
|
||||||
|
|
||||||
|
p1 = new Patient();
|
||||||
|
p1.addAddress().addLine("10 Main Street").addExtension(ext);
|
||||||
|
p2 = new Patient();
|
||||||
|
p2.addAddress().addLine("10 Main Street").addExtension(new Extension("demo", new DateTimeType("2021-01-02")));
|
||||||
|
|
||||||
|
TerserUtil.mergeField(ourFhirContext, "address", p1, p2);
|
||||||
|
assertEquals(2, p2.getAddress().size());
|
||||||
|
assertTrue(p2.getAddress().get(0).hasExtension());
|
||||||
|
assertTrue(p2.getAddress().get(1).hasExtension());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testReplaceForAddressWithExtensions() {
|
||||||
|
Extension ext = new Extension();
|
||||||
|
ext.setUrl("http://hapifhir.io/extensions/address#create-timestamp");
|
||||||
|
ext.setValue(new DateTimeType("2021-01-02T11:13:15"));
|
||||||
|
|
||||||
|
Patient p1 = new Patient();
|
||||||
|
p1.addAddress()
|
||||||
|
.addLine("10 Main Street")
|
||||||
|
.setCity("Hamilton")
|
||||||
|
.setState("ON")
|
||||||
|
.setPostalCode("Z0Z0Z0")
|
||||||
|
.setCountry("Canada")
|
||||||
|
.addExtension(ext);
|
||||||
|
|
||||||
|
Patient p2 = new Patient();
|
||||||
|
p2.addAddress().addLine("10 Lenin Street").setCity("Severodvinsk").setCountry("Russia");
|
||||||
|
|
||||||
|
TerserUtil.replaceField(ourFhirContext, "address", p1, p2);
|
||||||
|
|
||||||
|
assertEquals(1, p2.getAddress().size());
|
||||||
|
assertEquals("[10 Main Street]", p2.getAddress().get(0).getLine().toString());
|
||||||
|
assertTrue(p2.getAddress().get(0).hasExtension());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMergeForSimilarAddresses() {
|
||||||
|
Extension ext = new Extension();
|
||||||
|
ext.setUrl("http://hapifhir.io/extensions/address#create-timestamp");
|
||||||
|
ext.setValue(new DateTimeType("2021-01-02T11:13:15"));
|
||||||
|
|
||||||
|
Patient p1 = new Patient();
|
||||||
|
p1.addAddress()
|
||||||
|
.addLine("10 Main Street")
|
||||||
|
.setCity("Hamilton")
|
||||||
|
.setState("ON")
|
||||||
|
.setPostalCode("Z0Z0Z0")
|
||||||
|
.setCountry("Canada")
|
||||||
|
.addExtension(ext);
|
||||||
|
|
||||||
|
Patient p2 = new Patient();
|
||||||
|
p2.addAddress()
|
||||||
|
.addLine("10 Main Street")
|
||||||
|
.setCity("Hamilton")
|
||||||
|
.setState("ON")
|
||||||
|
.setPostalCode("Z0Z0Z1")
|
||||||
|
.setCountry("Canada")
|
||||||
|
.addExtension(ext);
|
||||||
|
|
||||||
|
TerserUtil.mergeField(ourFhirContext, "address", p1, p2);
|
||||||
|
|
||||||
|
assertEquals(2, p2.getAddress().size());
|
||||||
|
assertEquals("[10 Main Street]", p2.getAddress().get(0).getLine().toString());
|
||||||
|
assertEquals("[10 Main Street]", p2.getAddress().get(1).getLine().toString());
|
||||||
|
assertTrue(p2.getAddress().get(1).hasExtension());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCloneWithDuplicateNonPrimitives() {
|
||||||
|
Patient p1 = new Patient();
|
||||||
|
Patient p2 = new Patient();
|
||||||
|
|
||||||
|
p1.addName().addGiven("Jim");
|
||||||
|
p1.getNameFirstRep().addGiven("George");
|
||||||
|
|
||||||
|
assertThat(p1.getName(), hasSize(1));
|
||||||
|
assertThat(p1.getName().get(0).getGiven(), hasSize(2));
|
||||||
|
|
||||||
|
p2.addName().addGiven("Jim");
|
||||||
|
p2.getNameFirstRep().addGiven("George");
|
||||||
|
|
||||||
|
assertThat(p2.getName(), hasSize(1));
|
||||||
|
assertThat(p2.getName().get(0).getGiven(), hasSize(2));
|
||||||
|
|
||||||
|
TerserUtil.mergeAllFields(ourFhirContext, p1, p2);
|
||||||
|
|
||||||
|
assertThat(p2.getName(), hasSize(1));
|
||||||
|
assertThat(p2.getName().get(0).getGiven(), hasSize(2));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue