Merge branch 'master' of https://github.com/hapifhir/hapi-fhir into 2478_add_extension_mdm_matcher

This commit is contained in:
long 2021-03-17 10:01:56 -06:00
commit 660777f855
74 changed files with 6173 additions and 304 deletions

View File

@ -131,7 +131,7 @@ public abstract class BaseParser implements IParser {
} }
@Override @Override
public IParser setDontEncodeElements(Set<String> theDontEncodeElements) { public IParser setDontEncodeElements(Collection<String> theDontEncodeElements) {
if (theDontEncodeElements == null || theDontEncodeElements.isEmpty()) { if (theDontEncodeElements == null || theDontEncodeElements.isEmpty()) {
myDontEncodeElements = null; myDontEncodeElements = null;
} else { } else {

View File

@ -249,7 +249,7 @@ public interface IParser {
* @param theDontEncodeElements The elements to encode * @param theDontEncodeElements The elements to encode
* @see #setEncodeElements(Set) * @see #setEncodeElements(Set)
*/ */
IParser setDontEncodeElements(Set<String> theDontEncodeElements); IParser setDontEncodeElements(Collection<String> theDontEncodeElements);
/** /**
* If provided, specifies the elements which should be encoded, to the exclusion of all others. Valid values for this * If provided, specifies the elements which should be encoded, to the exclusion of all others. Valid values for this
@ -264,7 +264,7 @@ public interface IParser {
* </ul> * </ul>
* *
* @param theEncodeElements The elements to encode * @param theEncodeElements The elements to encode
* @see #setDontEncodeElements(Set) * @see #setDontEncodeElements(Collection)
*/ */
IParser setEncodeElements(Set<String> theEncodeElements); IParser setEncodeElements(Set<String> theEncodeElements);

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -24,6 +24,8 @@ import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options; import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException; import org.apache.commons.cli.ParseException;
@ -37,7 +39,6 @@ import org.hl7.fhir.utilities.npm.PackageGenerator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.print.attribute.standard.MediaSize;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -67,7 +68,7 @@ public class CreatePackageCommand extends BaseCommand {
@Override @Override
public String getCommandDescription() { public String getCommandDescription() {
return "Create an NPM package using the FHIR packging format"; return "Create an NPM package using the FHIR packaging format";
} }
@Override @Override
@ -129,6 +130,7 @@ public class CreatePackageCommand extends BaseCommand {
manifestGenerator.name(myPackageName); manifestGenerator.name(myPackageName);
manifestGenerator.version(myPackageVersion); manifestGenerator.version(myPackageVersion);
manifestGenerator.description(myPackageDescription); manifestGenerator.description(myPackageDescription);
injectFhirVersionsArray(manifestGenerator);
if (isNotBlank(myPackageDescription)) { if (isNotBlank(myPackageDescription)) {
manifestGenerator.description(myPackageDescription); manifestGenerator.description(myPackageDescription);
} }
@ -177,6 +179,13 @@ public class CreatePackageCommand extends BaseCommand {
} }
} }
private void injectFhirVersionsArray(PackageGenerator manifestGenerator) {
JsonObject rootJsonObject = manifestGenerator.getRootJsonObject();
JsonArray fhirVersionsArray = new JsonArray();
fhirVersionsArray.add(myFhirCtx.getVersion().getVersion().getFhirVersionString());
rootJsonObject.add("fhirVersions", fhirVersionsArray);
}
public void addFiles(String[] thePackageValues, String theFolder) throws ParseException, ExecutionException { public void addFiles(String[] thePackageValues, String theFolder) throws ParseException, ExecutionException {
if (thePackageValues != null) { if (thePackageValues != null) {
for (String nextPackageValue : thePackageValues) { for (String nextPackageValue : thePackageValues) {

View File

@ -101,6 +101,9 @@ public class CreatePackageCommandTest extends BaseTest {
String expectedPackageJson = "{\n" + String expectedPackageJson = "{\n" +
" \"name\": \"com.example.ig\",\n" + " \"name\": \"com.example.ig\",\n" +
" \"version\": \"1.0.1\",\n" + " \"version\": \"1.0.1\",\n" +
" \"fhirVersions\": [\n" +
" \"4.0.1\"\n" +
" ],\n" +
" \"dependencies\": {\n" + " \"dependencies\": {\n" +
" \"hl7.fhir.core\": \"4.0.1\",\n" + " \"hl7.fhir.core\": \"4.0.1\",\n" +
" \"foo.bar\": \"1.2.3\"\n" + " \"foo.bar\": \"1.2.3\"\n" +
@ -111,7 +114,6 @@ public class CreatePackageCommandTest extends BaseTest {
// Try parsing the module again to make sure we can // Try parsing the module again to make sure we can
NpmPackage loadedPackage = NpmPackage.fromPackage(new FileInputStream(igArchive)); NpmPackage loadedPackage = NpmPackage.fromPackage(new FileInputStream(igArchive));
assertEquals("com.example.ig", loadedPackage.name()); assertEquals("com.example.ig", loadedPackage.name());
} }
@Test @Test
@ -152,7 +154,10 @@ public class CreatePackageCommandTest extends BaseTest {
String expectedPackageJson = "{\n" + String expectedPackageJson = "{\n" +
" \"name\": \"com.example.ig\",\n" + " \"name\": \"com.example.ig\",\n" +
" \"version\": \"1.0.1\"\n" + " \"version\": \"1.0.1\",\n" +
" \"fhirVersions\": [\n" +
" \"4.0.1\"\n" +
" ]\n" +
"}"; "}";
assertEquals(expectedPackageJson, packageJsonContents); assertEquals(expectedPackageJson, packageJsonContents);

View File

@ -349,6 +349,16 @@
"lat": 37.7826622, "lat": 37.7826622,
"lon": -122.3983786, "lon": -122.3983786,
"added": "2020-05-06" "added": "2020-05-06"
} },{
"title": "MedPlat",
"description": "A Comprehensive Health Platform",
"link": "https://argusoft.com",
"contactName": "Kunjan Patel",
"contactEmail": "kunjanp@argusoft.com",
"city": "Gandhinagar,India",
"lat": 23.2420,
"lon": 72.6272,
"added": "2021-03-17"
}
] ]
} }

View File

@ -0,0 +1,4 @@
---
type: fix
issue: 1644
title: "support Oracle specific syntax when using a TermValueSetConceptView"

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 1731
title: "When storing resources in the JPA server, extensions in `Resource.meta` were not preserved, nor were
any contents in `Bundle.entry.resource.meta`. Both of these things are now correctly persisted and
returned. Thanks to Sean McIlvenna for reporting!"

View File

@ -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"

View File

@ -0,0 +1,4 @@
---
type: fix
issue: 2479
title: "the create-package command of the HAPI FHIR CLI was not correctly adding the `fhirVersions` section to the generated package.json. This has been fixed"

View File

@ -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.

View File

@ -51,7 +51,6 @@ import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper; import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
@ -96,6 +95,7 @@ import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseCoding; import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions; import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseMetaType; import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseReference;
@ -511,11 +511,64 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
if (thePerformIndexing) { if (thePerformIndexing) {
encoding = myConfig.getResourceEncoding(); encoding = myConfig.getResourceEncoding();
Set<String> excludeElements = ResourceMetaParams.EXCLUDE_ELEMENTS_IN_ENCODED;
String resourceType = theEntity.getResourceType();
List<String> excludeElements = new ArrayList<>(8);
excludeElements.add("id");
IBaseMetaType meta = theResource.getMeta();
boolean hasExtensions = false;
IBaseExtension<?,?> sourceExtension = null;
if (meta instanceof IBaseHasExtensions) {
List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) meta).getExtension();
if (!extensions.isEmpty()) {
hasExtensions = true;
/*
* FHIR DSTU3 did not have the Resource.meta.source field, so we use a
* custom HAPI FHIR extension in Resource.meta to store that field. However,
* we put the value for that field in a separate table so we don't want to serialize
* it into the stored BLOB. Therefore: remove it from the resource temporarily
* and restore it afterward.
*/
if (myFhirContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
for (int i = 0; i < extensions.size(); i++) {
if (extensions.get(i).getUrl().equals(HapiExtensions.EXT_META_SOURCE)) {
sourceExtension = extensions.remove(i);
i--;
}
}
}
}
}
if (hasExtensions) {
excludeElements.add(resourceType + ".meta.profile");
excludeElements.add(resourceType + ".meta.tag");
excludeElements.add(resourceType + ".meta.security");
excludeElements.add(resourceType + ".meta.versionId");
excludeElements.add(resourceType + ".meta.lastUpdated");
excludeElements.add(resourceType + ".meta.source");
} else {
/*
* If there are no extensions in the meta element, we can just exclude the
* whole meta element, which avoids adding an empty "meta":{}
* from showing up in the serialized JSON.
*/
excludeElements.add(resourceType + ".meta");
}
theEntity.setFhirVersion(myContext.getVersion().getVersion()); theEntity.setFhirVersion(myContext.getVersion().getVersion());
bytes = encodeResource(theResource, encoding, excludeElements, myContext); bytes = encodeResource(theResource, encoding, excludeElements, myContext);
if (sourceExtension != null) {
IBaseExtension<?, ?> newSourceExtension = ((IBaseHasExtensions) meta).addExtension();
newSourceExtension.setUrl(sourceExtension.getUrl());
newSourceExtension.setValue(sourceExtension.getValue());
}
HashFunction sha256 = Hashing.sha256(); HashFunction sha256 = Hashing.sha256();
String hashSha256 = sha256.hashBytes(bytes).toString(); String hashSha256 = sha256.hashBytes(bytes).toString();
if (hashSha256.equals(theEntity.getHashSha256()) == false) { if (hashSha256.equals(theEntity.getHashSha256()) == false) {
@ -1530,7 +1583,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
return resourceText; return resourceText;
} }
public static byte[] encodeResource(IBaseResource theResource, ResourceEncodingEnum theEncoding, Set<String> theExcludeElements, FhirContext theContext) { public static byte[] encodeResource(IBaseResource theResource, ResourceEncodingEnum theEncoding, List<String> theExcludeElements, FhirContext theContext) {
byte[] bytes; byte[] bytes;
IParser parser = theEncoding.newParser(theContext); IParser parser = theEncoding.newParser(theContext);
parser.setDontEncodeElements(theExcludeElements); parser.setDontEncodeElements(theExcludeElements);

View File

@ -0,0 +1,17 @@
package ca.uhn.fhir.jpa.dao.data;
import ca.uhn.fhir.jpa.entity.TermValueSetConceptViewOracle;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.io.Serializable;
import java.util.List;
public interface ITermValueSetConceptViewOracleDao extends JpaRepository<TermValueSetConceptViewOracle, Long> {
@Query("SELECT v FROM TermValueSetConceptView v WHERE v.myConceptValueSetPid = :pid AND v.myConceptOrder >= :from AND v.myConceptOrder < :to ORDER BY v.myConceptOrder")
List<TermValueSetConceptViewOracle> findByTermValueSetId(@Param("from") int theFrom, @Param("to") int theTo, @Param("pid") Long theValueSetId);
@Query("SELECT v FROM TermValueSetConceptView v WHERE v.myConceptValueSetPid = :pid AND LOWER(v.myConceptDisplay) LIKE :display ORDER BY v.myConceptOrder")
List<TermValueSetConceptViewOracle> findByTermValueSetId(@Param("pid") Long theValueSetId, @Param("display") String theDisplay);
}

View File

@ -0,0 +1,142 @@
package ca.uhn.fhir.jpa.entity;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* 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.hibernate.annotations.Immutable;
import org.hibernate.annotations.Subselect;
import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Immutable
@Subselect(
/*
* Note about the CONCAT function below- We need a primary key (an @Id) column
* because hibernate won't allow the view the function without it, but
*/
"SELECT CONCAT(vsc.PID, CONCAT(' ', vscd.PID)) AS PID, " +
" vsc.PID AS CONCEPT_PID, " +
" vsc.VALUESET_PID AS CONCEPT_VALUESET_PID, " +
" vsc.VALUESET_ORDER AS CONCEPT_VALUESET_ORDER, " +
" vsc.SYSTEM_URL AS CONCEPT_SYSTEM_URL, " +
" vsc.CODEVAL AS CONCEPT_CODEVAL, " +
" vsc.DISPLAY AS CONCEPT_DISPLAY, " +
" vscd.PID AS DESIGNATION_PID, " +
" vscd.LANG AS DESIGNATION_LANG, " +
" vscd.USE_SYSTEM AS DESIGNATION_USE_SYSTEM, " +
" vscd.USE_CODE AS DESIGNATION_USE_CODE, " +
" vscd.USE_DISPLAY AS DESIGNATION_USE_DISPLAY, " +
" vscd.VAL AS DESIGNATION_VAL " +
"FROM TRM_VALUESET_CONCEPT vsc " +
"LEFT OUTER JOIN TRM_VALUESET_C_DESIGNATION vscd ON vsc.PID = vscd.VALUESET_CONCEPT_PID"
)
public class TermValueSetConceptViewOracle implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name="PID", length = 1000 /* length only needed to satisfy JpaEntityTest, it's not used*/)
private String id; // still set automatically
@Column(name = "CONCEPT_PID")
private Long myConceptPid;
@Column(name = "CONCEPT_VALUESET_PID")
private Long myConceptValueSetPid;
@Column(name = "CONCEPT_VALUESET_ORDER")
private int myConceptOrder;
@Column(name = "CONCEPT_SYSTEM_URL", length = TermCodeSystem.MAX_URL_LENGTH)
private String myConceptSystemUrl;
@Column(name = "CONCEPT_CODEVAL", length = TermConcept.MAX_CODE_LENGTH)
private String myConceptCode;
@Column(name = "CONCEPT_DISPLAY", length = TermConcept.MAX_DESC_LENGTH)
private String myConceptDisplay;
@Column(name = "DESIGNATION_PID")
private Long myDesignationPid;
@Column(name = "DESIGNATION_LANG", length = TermConceptDesignation.MAX_LENGTH)
private String myDesignationLang;
@Column(name = "DESIGNATION_USE_SYSTEM", length = TermConceptDesignation.MAX_LENGTH)
private String myDesignationUseSystem;
@Column(name = "DESIGNATION_USE_CODE", length = TermConceptDesignation.MAX_LENGTH)
private String myDesignationUseCode;
@Column(name = "DESIGNATION_USE_DISPLAY", length = TermConceptDesignation.MAX_LENGTH)
private String myDesignationUseDisplay;
@Column(name = "DESIGNATION_VAL", length = TermConceptDesignation.MAX_VAL_LENGTH)
private String myDesignationVal;
public Long getConceptPid() {
return myConceptPid;
}
public String getConceptSystemUrl() {
return myConceptSystemUrl;
}
public String getConceptCode() {
return myConceptCode;
}
public String getConceptDisplay() {
return myConceptDisplay;
}
public Long getDesignationPid() {
return myDesignationPid;
}
public String getDesignationLang() {
return myDesignationLang;
}
public String getDesignationUseSystem() {
return myDesignationUseSystem;
}
public String getDesignationUseCode() {
return myDesignationUseCode;
}
public String getDesignationUseDisplay() {
return myDesignationUseDisplay;
}
public String getDesignationVal() {
return myDesignationVal;
}
}

View File

@ -45,6 +45,7 @@ import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao; import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDesignationDao; import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDesignationDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptViewDao; import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptViewDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptViewOracleDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao; import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao;
import ca.uhn.fhir.jpa.entity.TermCodeSystem; import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
@ -62,6 +63,7 @@ import ca.uhn.fhir.jpa.entity.TermConceptPropertyTypeEnum;
import ca.uhn.fhir.jpa.entity.TermValueSet; import ca.uhn.fhir.jpa.entity.TermValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSetConcept; import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
import ca.uhn.fhir.jpa.entity.TermValueSetConceptView; import ca.uhn.fhir.jpa.entity.TermValueSetConceptView;
import ca.uhn.fhir.jpa.entity.TermValueSetConceptViewOracle;
import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum; import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.sched.HapiJob; import ca.uhn.fhir.jpa.model.sched.HapiJob;
@ -246,6 +248,8 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
@Autowired @Autowired
private ITermValueSetConceptViewDao myTermValueSetConceptViewDao; private ITermValueSetConceptViewDao myTermValueSetConceptViewDao;
@Autowired @Autowired
private ITermValueSetConceptViewOracleDao myTermValueSetConceptViewOracleDao;
@Autowired
private ISchedulerService mySchedulerService; private ISchedulerService mySchedulerService;
@Autowired(required = false) @Autowired(required = false)
private ITermDeferredStorageSvc myDeferredStorageSvc; private ITermDeferredStorageSvc myDeferredStorageSvc;
@ -527,11 +531,126 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
*/ */
String msg = myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "valueSetExpandedUsingPreExpansion"); String msg = myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "valueSetExpandedUsingPreExpansion");
theAccumulator.addMessage(msg); theAccumulator.addMessage(msg);
if (isOracleDialect()) {
expandConceptsOracle(theAccumulator, termValueSet, theFilter, theAdd);
}
else {
expandConcepts(theAccumulator, termValueSet, theFilter, theAdd); expandConcepts(theAccumulator, termValueSet, theFilter, theAdd);
} }
}
private boolean isOracleDialect(){
return myHibernatePropertiesProvider.getDialect() instanceof org.hibernate.dialect.Oracle12cDialect;
}
private void expandConceptsOracle(IValueSetConceptAccumulator theAccumulator, TermValueSet theTermValueSet, ExpansionFilter theFilter, boolean theAdd) {
// Literal copy paste from expandConcepts but tailored for Oracle since we can't reliably extend the DAO and hibernate classes
Integer offset = theAccumulator.getSkipCountRemaining();
offset = ObjectUtils.defaultIfNull(offset, 0);
offset = Math.min(offset, theTermValueSet.getTotalConcepts().intValue());
Integer count = theAccumulator.getCapacityRemaining();
count = defaultIfNull(count, myDaoConfig.getMaximumExpansionSize());
int conceptsExpanded = 0;
int designationsExpanded = 0;
int toIndex = offset + count;
Collection<TermValueSetConceptViewOracle> conceptViews;
boolean wasFilteredResult = false;
String filterDisplayValue = null;
if (!theFilter.getFilters().isEmpty() && JpaConstants.VALUESET_FILTER_DISPLAY.equals(theFilter.getFilters().get(0).getProperty()) && theFilter.getFilters().get(0).getOp() == ValueSet.FilterOperator.EQUAL) {
filterDisplayValue = lowerCase(theFilter.getFilters().get(0).getValue().replace("%", "[%]"));
String displayValue = "%" + lowerCase(filterDisplayValue) + "%";
conceptViews = myTermValueSetConceptViewOracleDao.findByTermValueSetId(theTermValueSet.getId(), displayValue);
wasFilteredResult = true;
} else {
// TODO JA HS: I'm pretty sure we are overfetching here. test says offset 3, count 4, but we are fetching index 3 -> 10 here, grabbing 7 concepts.
//Specifically this test testExpandInline_IncludePreExpandedValueSetByUri_FilterOnDisplay_LeftMatch_SelectRange
conceptViews = myTermValueSetConceptViewOracleDao.findByTermValueSetId(offset, toIndex, theTermValueSet.getId());
theAccumulator.consumeSkipCount(offset);
if (theAdd) {
theAccumulator.incrementOrDecrementTotalConcepts(true, theTermValueSet.getTotalConcepts().intValue());
}
}
if (conceptViews.isEmpty()) {
logConceptsExpanded("No concepts to expand. ", theTermValueSet, conceptsExpanded);
return;
}
Map<Long, FhirVersionIndependentConcept> pidToConcept = new LinkedHashMap<>();
ArrayListMultimap<Long, TermConceptDesignation> pidToDesignations = ArrayListMultimap.create();
for (TermValueSetConceptViewOracle conceptView : conceptViews) {
String system = conceptView.getConceptSystemUrl();
String code = conceptView.getConceptCode();
String display = conceptView.getConceptDisplay();
//-- this is quick solution, may need to revisit
if (!applyFilter(display, filterDisplayValue))
continue;
Long conceptPid = conceptView.getConceptPid();
if (!pidToConcept.containsKey(conceptPid)) {
FhirVersionIndependentConcept concept = new FhirVersionIndependentConcept(system, code, display);
pidToConcept.put(conceptPid, concept);
}
// TODO: DM 2019-08-17 - Implement includeDesignations parameter for $expand operation to designations optional.
if (conceptView.getDesignationPid() != null) {
TermConceptDesignation designation = new TermConceptDesignation();
designation.setUseSystem(conceptView.getDesignationUseSystem());
designation.setUseCode(conceptView.getDesignationUseCode());
designation.setUseDisplay(conceptView.getDesignationUseDisplay());
designation.setValue(conceptView.getDesignationVal());
designation.setLanguage(conceptView.getDesignationLang());
pidToDesignations.put(conceptPid, designation);
if (++designationsExpanded % 250 == 0) {
logDesignationsExpanded("Expansion of designations in progress. ", theTermValueSet, designationsExpanded);
}
}
if (++conceptsExpanded % 250 == 0) {
logConceptsExpanded("Expansion of concepts in progress. ", theTermValueSet, conceptsExpanded);
}
}
for (Long nextPid : pidToConcept.keySet()) {
FhirVersionIndependentConcept concept = pidToConcept.get(nextPid);
List<TermConceptDesignation> designations = pidToDesignations.get(nextPid);
String system = concept.getSystem();
String code = concept.getCode();
String display = concept.getDisplay();
if (theAdd) {
if (theAccumulator.getCapacityRemaining() != null) {
if (theAccumulator.getCapacityRemaining() == 0) {
break;
}
}
theAccumulator.includeConceptWithDesignations(system, code, display, designations);
} else {
boolean removed = theAccumulator.excludeConcept(system, code);
if (removed) {
theAccumulator.incrementOrDecrementTotalConcepts(false, 1);
}
}
}
if (wasFilteredResult && theAdd) {
theAccumulator.incrementOrDecrementTotalConcepts(true, pidToConcept.size());
}
logDesignationsExpanded("Finished expanding designations. ", theTermValueSet, designationsExpanded);
logConceptsExpanded("Finished expanding concepts. ", theTermValueSet, conceptsExpanded);
}
private void expandConcepts(IValueSetConceptAccumulator theAccumulator, TermValueSet theTermValueSet, ExpansionFilter theFilter, boolean theAdd) { private void expandConcepts(IValueSetConceptAccumulator theAccumulator, TermValueSet theTermValueSet, ExpansionFilter theFilter, boolean theAdd) {
// NOTE: if you modifiy the logic here, look to `expandConceptsOracle` and see if your new code applies to its copy pasted sibling
Integer offset = theAccumulator.getSkipCountRemaining(); Integer offset = theAccumulator.getSkipCountRemaining();
offset = ObjectUtils.defaultIfNull(offset, 0); offset = ObjectUtils.defaultIfNull(offset, 0);
offset = Math.min(offset, theTermValueSet.getTotalConcepts().intValue()); offset = Math.min(offset, theTermValueSet.getTotalConcepts().intValue());
@ -544,6 +663,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
int toIndex = offset + count; int toIndex = offset + count;
Collection<TermValueSetConceptView> conceptViews; Collection<TermValueSetConceptView> conceptViews;
Collection<TermValueSetConceptViewOracle> conceptViewsOracle;
boolean wasFilteredResult = false; boolean wasFilteredResult = false;
String filterDisplayValue = null; String filterDisplayValue = null;
if (!theFilter.getFilters().isEmpty() && JpaConstants.VALUESET_FILTER_DISPLAY.equals(theFilter.getFilters().get(0).getProperty()) && theFilter.getFilters().get(0).getOp() == ValueSet.FilterOperator.EQUAL) { if (!theFilter.getFilters().isEmpty() && JpaConstants.VALUESET_FILTER_DISPLAY.equals(theFilter.getFilters().get(0).getProperty()) && theFilter.getFilters().get(0).getOp() == ValueSet.FilterOperator.EQUAL) {

View File

@ -0,0 +1,126 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.SampledData;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.PageRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class FhirResourceDaoR4MetaTest extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4MetaTest.class);
/**
* See #1731
*/
@Test
public void testMetaExtensionsPreserved() {
Patient patient = new Patient();
patient.setActive(true);
patient.getMeta().addExtension("http://foo", new StringType("hello"));
IIdType id = myPatientDao.create(patient).getId();
patient = myPatientDao.read(id);
assertTrue(patient.getActive());
assertEquals(1, patient.getMeta().getExtensionsByUrl("http://foo").size());
assertEquals("hello", patient.getMeta().getExtensionByUrl("http://foo").getValueAsPrimitive().getValueAsString());
}
/**
* See #1731
*/
@Test
public void testBundleInnerResourceMetaIsPreserved() {
Patient patient = new Patient();
patient.setActive(true);
patient.getMeta().setLastUpdatedElement(new InstantType("2011-01-01T12:12:12Z"));
patient.getMeta().setVersionId("22");
patient.getMeta().addProfile("http://foo");
patient.getMeta().addTag("http://tag", "value", "the tag");
patient.getMeta().addSecurity("http://tag", "security", "the tag");
patient.getMeta().addExtension("http://foo", new StringType("hello"));
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.COLLECTION);
bundle.addEntry().setResource(patient);
IIdType id = myBundleDao.create(bundle).getId();
bundle = myBundleDao.read(id);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
patient = (Patient) bundle.getEntryFirstRep().getResource();
assertTrue(patient.getActive());
assertEquals(1, patient.getMeta().getExtensionsByUrl("http://foo").size());
assertEquals("22", patient.getMeta().getVersionId());
assertEquals("http://foo", patient.getMeta().getProfile().get(0).getValue());
assertEquals("hello", patient.getMeta().getExtensionByUrl("http://foo").getValueAsPrimitive().getValueAsString());
}
/**
* See #1731
*/
@Test
public void testMetaValuesNotStoredAfterDeletion() {
Patient patient = new Patient();
patient.setActive(true);
patient.getMeta().addProfile("http://foo");
patient.getMeta().addTag("http://tag", "value", "the tag");
patient.getMeta().addSecurity("http://tag", "security", "the tag");
IIdType id = myPatientDao.create(patient).getId();
Meta meta = new Meta();
meta.addProfile("http://foo");
meta.addTag("http://tag", "value", "the tag");
meta.addSecurity("http://tag", "security", "the tag");
myPatientDao.metaDeleteOperation(id, meta, mySrd);
patient = myPatientDao.read(id);
assertThat(patient.getMeta().getProfile(), empty());
assertThat(patient.getMeta().getTag(), empty());
assertThat(patient.getMeta().getSecurity(), empty());
}
}

View File

@ -64,7 +64,9 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.leftPad; import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.blankOrNullString; import static org.hamcrest.Matchers.blankOrNullString;
@ -622,6 +624,12 @@ public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProvid
// The paging should have ended now - but the last redacted female result is an empty existing page which should never have been there. // The paging should have ended now - but the last redacted female result is an empty existing page which should never have been there.
assertNotNull(BundleUtil.getLinkUrlOfType(myFhirCtx, response, "next")); assertNotNull(BundleUtil.getLinkUrlOfType(myFhirCtx, response, "next"));
await()
.until(
()->mySearchEntityDao.findByUuidAndFetchIncludes(searchId).orElseThrow(() -> new IllegalStateException()).getStatus(),
equalTo(SearchStatusEnum.FINISHED)
);
runInTransaction(() -> { runInTransaction(() -> {
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(searchId).orElseThrow(() -> new IllegalStateException()); Search search = mySearchEntityDao.findByUuidAndFetchIncludes(searchId).orElseThrow(() -> new IllegalStateException());
assertEquals(3, search.getNumFound()); assertEquals(3, search.getNumFound());

View File

@ -10,6 +10,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
@ -32,6 +33,7 @@ public class ResourceProviderExpungeR4Test extends BaseResourceProviderR4Test {
@AfterEach @AfterEach
public void afterDisableExpunge() { public void afterDisableExpunge() {
myDaoConfig.setExpungeEnabled(new DaoConfig().isExpungeEnabled()); myDaoConfig.setExpungeEnabled(new DaoConfig().isExpungeEnabled());
myDaoConfig.setEnforceReferentialIntegrityOnDelete(false);
} }
private void assertExpunged(IIdType theId) { private void assertExpunged(IIdType theId) {
@ -120,6 +122,9 @@ public class ResourceProviderExpungeR4Test extends BaseResourceProviderR4Test {
case "Observation": case "Observation":
dao = myObservationDao; dao = myObservationDao;
break; break;
case "Organization":
dao = myOrganizationDao;
break;
default: default:
fail("Restype: " + theId.getResourceType()); fail("Restype: " + theId.getResourceType());
dao = myPatientDao; dao = myPatientDao;
@ -324,5 +329,51 @@ public class ResourceProviderExpungeR4Test extends BaseResourceProviderR4Test {
} }
/**
* See #2015
*/
@Test
public void testExpungeSucceedsWithIncomingReferences_ReferentialIntegrityDisabled() {
myDaoConfig.setEnforceReferentialIntegrityOnDelete(false);
Organization org = new Organization();
org.setActive(true);
IIdType orgId = myOrganizationDao.create(org).getId();
Patient patient = new Patient();
patient.setActive(true);
patient.getManagingOrganization().setReference(orgId.toUnqualifiedVersionless().getValue());
myPatientDao.create(patient);
runInTransaction(()-> assertEquals(1, myResourceLinkDao.count()));
myOrganizationDao.delete(orgId);
runInTransaction(()-> assertEquals(0, myResourceLinkDao.count()));
Parameters input = new Parameters();
input.addParameter()
.setName(JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT)
.setValue(new IntegerType(1000));
input.addParameter()
.setName(JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES)
.setValue(new BooleanType(true));
input.addParameter()
.setName(JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS)
.setValue(new BooleanType(true));
myClient
.operation()
.onInstance(orgId.toUnqualifiedVersionless())
.named("expunge")
.withParameters(input)
.execute();
assertExpunged(orgId.toUnqualifiedVersionless());
runInTransaction(()-> assertEquals(0, myResourceLinkDao.count()));
}
} }

View File

@ -35,9 +35,7 @@ import org.hl7.fhir.instance.model.api.IAnyResource;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
public class ResourceMetaParams { public class ResourceMetaParams {
/** /**
@ -48,7 +46,6 @@ public class ResourceMetaParams {
* These are parameters which are supported by searches * These are parameters which are supported by searches
*/ */
public static final Map<String, Class<? extends IQueryParameterType>> RESOURCE_META_PARAMS; public static final Map<String, Class<? extends IQueryParameterType>> RESOURCE_META_PARAMS;
public static final Set<String> EXCLUDE_ELEMENTS_IN_ENCODED;
static { static {
Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<>(); Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<>();
@ -67,10 +64,5 @@ public class ResourceMetaParams {
resourceMetaAndParams.put(Constants.PARAM_HAS, HasAndListParam.class); resourceMetaAndParams.put(Constants.PARAM_HAS, HasAndListParam.class);
RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams); RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams);
RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams); RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams);
HashSet<String> excludeElementsInEncoded = new HashSet<>();
excludeElementsInEncoded.add("id");
excludeElementsInEncoded.add("*.meta");
EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded);
} }
} }

View File

@ -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 = "*";

View File

@ -73,6 +73,7 @@ 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 <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 theIncomingResource The resource that will be used as the starting point for the MDM linking.
* @param theMdmTransactionContext * @param 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);
}
} }

View File

@ -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;
} }
} }

View File

@ -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();

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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, ' '); // &nbsp
addTranslate((int) ' ', ' '); // &nbsp
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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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 + '\'';
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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("");
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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=

View File

@ -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"
}
}

View File

@ -0,0 +1,3 @@
{
"telecom.where(system='email').value" : "EMAIL"
}

View File

@ -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

View File

@ -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) {
}
}
}

View File

@ -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"));
}
}

View File

@ -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"));
}
}

View File

@ -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"));
}
}

View File

@ -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);
});
}
}
}

View File

@ -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(""));
}
}

View File

@ -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]));
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,2 @@
The tests for AddressHelper, StandardizingInterceptor, AddressValidatingInterceptor, FieldValidatingInterceptor
and TerserUtils are in "hapi-fhir-structures-r4" to avoid issues with dependencies.

View File

@ -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
1 1328746 Ont Ltd / Independent Taxi 1328746 ONT LTD / INDEPENDENT TAXI
2 20/20 Massage 20/20 Massage
3 20/20 Optometrists 20/20 OPTOMETRISTS
4 20/20 Vision 20/20 VISION
5 20/20 Vision Care 20/20 VISION CARE
6 20/20 Vision Care Inc. 20/20 VISION CARE INC.
7 20/20 Vision Clinic Inc 20/20 VISION CLINIC INC
8 2189450 Alberta Ltd O/A Revive & Rekindle Massage Therapy 2189450 ALBERTA LTD O/A REVIVE & REKINDLE MASSAGE THERAPY
9 2343571 Ontario Inc O/A Westmore Wellness Rehab Clinic 2343571 ONTARIO INC O/A WESTMORE WELLNESS REHAB CLINIC
10 497084 Ontario Ltd. O/A Woodys Wheels 497084 ONTARIO LTD. O/A WOODYS WHEELS
11 Achilles Orthopaedic Shoes/Medical Devices ACHILLES ORTHOPAEDIC SHOES/MEDICAL DEVICES
12 Action Sport Physio Saint-Eustache/Deux-Montagnes ACTION SPORT PHYSIO SAINT-EUSTACHE/DEUX-MONTAGNES
13 Agilec - Barrie/Orillia (EPS) AGILEC - BARRIE/ORILLIA (EPS)
14 Agilec - Guelph/Kitchener/Waterloo (EAS) AGILEC - GUELPH/KITCHENER/WATERLOO (EAS)
15 Agilec - Guelph/Kitchener/Waterloo (EPS) AGILEC - GUELPH/KITCHENER/WATERLOO (EPS)
16 Ags Rehab Solutions - Halton/Peel AGS REHAB SOLUTIONS - HALTON/PEEL
17 Ags Rehab Solutions Inc - Barrie/Orillia AGS REHAB SOLUTIONS INC - BARRIE/ORILLIA
18 Ags Rehab Solutions Inc - Thunder Bay/Dryden/Kenora AGS REHAB SOLUTIONS INC - THUNDER BAY/DRYDEN/KENORA
19 Ags Rehab Solutions Inc-Guelph/Kitchener/Waterloo AGS REHAB SOLUTIONS INC-GUELPH/KITCHENER/WATERLOO
20 Aladdin Geleidi O/A 1296864 Alberta Ltd ALADDIN GELEIDI O/A 1296864 ALBERTA LTD
21 Allan McGavin Sports Medicine Centre @ Usb ALLAN MCGAVIN SPORTS MEDICINE CENTRE @ USB
22 Allan McGavin Sports Medicine Centre Physiotherapy @ Plaza of Nations ALLAN MCGAVIN SPORTS MEDICINE CENTRE PHYSIOTHERAPY @ PLAZA OF NATIONS
23 Amber-Lynn O'Brien amber-lynn O€�brien
24 Angela Wing Wen Lam ANGELA WING WEN  LAM
25 Ascenseurs Lumar/Concord Quebec Inc. ASCENSEURS LUMAR/CONCORD QUEBEC INC.
26 Back in Sync: Wellness Centre BACK IN SYNC: WELLNESS CENTRE
27 Balance: Psychology and Brain Health BALANCE: PSYCHOLOGY AND BRAIN HEALTH
28 Bayshore Therapy and Rehab - Barrie/Orillia BAYSHORE THERAPY AND REHAB - BARRIE/ORILLIA
29 Bayshore Therapy and Rehab - Guelph/Kitchener/Waterloo BAYSHORE THERAPY AND REHAB - GUELPH/KITCHENER/WATERLOO
30 Bayshore Therapy and Rehab - Halton/Peel BAYSHORE THERAPY AND REHAB - HALTON/PEEL
31 Bayshore Therapy and Rehab - Thunder Bay/Dryden/Kenora BAYSHORE THERAPY AND REHAB - THUNDER BAY/DRYDEN/KENORA
32 Bing Siang Gan BING SIANG  GAN
33 Bio-Ped/2059779 Ontario Ltd BIO-PED/2059779 ONTARIO LTD
34 Bloor : Keele Chiropractic BLOOR : KEELE CHIROPRACTIC
35 Brampton/Bramalea Kwik Kab BRAMPTON/BRAMALEA KWIK KAB
36 Brock Community Health Centre/Nursing BROCK COMMUNITY HEALTH CENTRE/NURSING
37 C'Est la Vue ! C'EST LA VUE !
38 Can-Weld/Can-Fab CAN-WELD/CAN-FAB
39 Cbi - Langley / Oasis Sports Injury Centre CBI - LANGLEY / OASIS SPORTS INJURY CENTRE
40 Centre de Sante Communitaire Hamilton/Niagara CENTRE DE SANTE COMMUNITAIRE HAMILTON/NIAGARA
41 Cheelcare / 9302204 Canada Inc. CHEELCARE / 9302204 CANADA INC.
42 Clement's/Callander Ida Pharmacies CLEMENT'S/CALLANDER IDA PHARMACIES
43 Clsc/Chsld Des Etchemin CLSC/CHSLD DES ETCHEMIN
44 Cowichan Eyecare-Chemainus COWICHAN EYECARE~CHEMAINUS
45 Crawford Healthcare Management - Guelph/Kitchener CRAWFORD HEALTHCARE MANAGEMENT - GUELPH/KITCHENER
46 Daniel Man Tat Wong DANIEL MAN TAT  WONG
47 Dominique Nadon Ssuo/Uohs DOMINIQUE NADON SSUO/UOHS
48 Dormez-Vous / Sleep Country DORMEZ-VOUS / SLEEP COUNTRY
49 Dr Todd E Mazzuca O/A Northstone Chiropractic Paris St DR TODD E MAZZUCA O/A NORTHSTONE CHIROPRACTIC PARIS ST
50 Dr. Atoosa Chiropractor/Acupuncture DR. ATOOSA CHIROPRACTOR/ACUPUNCTURE
51 Dufferin Drug Mart O/A Pharmadx Drugs Ltd DUFFERIN DRUG MART O/A PHARMADX DRUGS LTD
52 Emergency Assoc/Univ of Roc EMERGENCY ASSOC/UNIV OF ROC
53 Eric Johnson/Hear at Last ERIC JOHNSON/HEAR AT LAST
54 Eyemate Vision/Crystal Clear Optical EYEMATE VISION/CRYSTAL CLEAR OPTICAL
55 Eyes Inspire / Visionworks EYES INSPIRE / VISIONWORKS
56 Feet First Pedorthic/Nursing Foot Care Clinic FEET FIRST PEDORTHIC/NURSING FOOT CARE CLINIC
57 Fine + Well: Health and Chiropractic FINE + WELL: HEALTH AND CHIROPRACTIC
58 Foot Solutions/2288564 Ontario Inc FOOT SOLUTIONS/2288564 ONTARIO INC
59 Friuli Benevolent Corp./Friuli Terrace FRIULI BENEVOLENT CORP./FRIULI TERRACE
60 Go! Physiotherapy Sports and Wellness Centre GO! PHYSIOTHERAPY SPORTS AND WELLNESS CENTRE
61 Gobi Ratnaswami Ganapathy GOBI RATNASWAMI  GANAPATHY
62 Gordon Josephson/Gilmour Psychological Services GORDON JOSEPHSON/GILMOUR PSYCHOLOGICAL SERVICES
63 Grand River Hospital - Kitchener/Waterloo Health Centre GRAND RIVER HOSPITAL - KITCHENER/WATERLOO HEALTH CENTRE
64 Green Tractors Clow Farm Equipment/ GREEN TRACTORS CLOW FARM EQUIPMENT/
65 H/Q Healthquest H/Q HEALTHQUEST
66 Hear at Last/Phc Canada Scarborough HEAR AT LAST/PHC CANADA SCARBOROUGH
67 Hearinglife - Sydney HEARINGLIFE – SYDNEY
68 Howard Chung C/O Lishan Management Co. Ltd HOWARD CHUNG C/O LISHAN MANAGEMENT CO. LTD
69 Ian Gray/ Sound Ideas Audiology IAN GRAY/ SOUND IDEAS AUDIOLOGY
70 Iris-292-Centre Piazzazzurri IRIS-292-CENTRE PIAZZ`AZZURRI
71 Ismp/Tanya Armstrong ISMP/TANYA ARMSTRONG
72 Jeffrey Thomas Hovey JEFFREY THOMAS  HOVEY
73 John D Franks/Access Hearing Care JOHN D FRANKS/ACCESS HEARING CARE
74 John David Jacques Bender JOHN DAVID JACQUES   BENDER
75 Kenneth Bernard Sabourin KENNETH BERNARD  SABOURIN
76 Killaloe Supermarket/Aj's Killaloe Convenience KILLALOE SUPERMARKET/AJ'S KILLALOE CONVENIENCE
77 Lakeshore General Hospitalc/Oaccounting Dept LAKESHORE GENERAL HOSPITALC/OACCOUNTING DEPT
78 Le Groupe Forget/Audioprothesistes LE GROUPE FORGET/AUDIOPROTHESISTES
79 Leaps and Bounds: Performance Rehabilitation LEAPS AND BOUNDS: PERFORMANCE REHABILITATION
80 Lifemark Health Corp - Barrie/Orillia LIFEMARK HEALTH CORP - BARRIE/ORILLIA
81 Lifemark Health Corp - Guelph/Kitchener/Waterloo LIFEMARK HEALTH CORP - GUELPH/KITCHENER/WATERLOO
82 Lifemark Health Corp - Halton / Peel LIFEMARK HEALTH CORP - HALTON / PEEL
83 Listenup! Canada - Dundas West LISTENUP! CANADA - DUNDAS WEST
84 Listenup! Canada - Toronto LISTENUP! CANADA - TORONTO
85 Longley/Vickar L.L.P. Barristers & Solicitors LONGLEY/VICKAR L.L.P. BARRISTERS & SOLICITORS
86 Lorraine McLeod O/A Union Taxi LORRAINE MCLEOD O/A UNION TAXI
87 Manpreet Birring MANPREET  BIRRING
88 March of Dimes Canada - Barrie/Orillia (EAS) MARCH OF DIMES CANADA - BARRIE/ORILLIA (EAS)
89 March of Dimes Canada - Barrie/Orillia (EPS) MARCH OF DIMES CANADA - BARRIE/ORILLIA (EPS)
90 March of Dimes Canada - Guelph/Kitchener/Waterloo (EAS) MARCH OF DIMES CANADA - GUELPH/KITCHENER/WATERLOO (EAS)
91 March of Dimes Canada - Guelph/Kitchener/Waterloo (EPS) MARCH OF DIMES CANADA - GUELPH/KITCHENER/WATERLOO (EPS)
92 March of Dimes Canada - Halton/Peel (EAS) MARCH OF DIMES CANADA - HALTON/PEEL (EAS)
93 March of Dimes Canada - Halton/Peel (EPS) MARCH OF DIMES CANADA - HALTON/PEEL (EPS)
94 March of Dimes Canada - Thunder Bay Dryden/Kenora (EAS) MARCH OF DIMES CANADA - THUNDER BAY DRYDEN/KENORA (EAS)
95 March of Dimes Canada - Thunder Bay Dryden/Kenora (EPS) MARCH OF DIMES CANADA - THUNDER BAY DRYDEN/KENORA (EPS)
96 Marion Baechler MARION  BAECHLER
97 Markham Family Medicine Teaching Unit/Health For All Fht MARKHAM FAMILY MEDICINE TEACHING UNIT/HEALTH FOR ALL FHT
98 Med-E-Ox/Mobility in Motion MED-E-OX/MOBILITY IN MOTION
99 Medcare Clinics @ Walmart Pen Centre MEDCARE CLINICS @ WALMART PEN CENTRE
100 Medical Center For Foot/Ankle MEDICAL CENTER FOR FOOT/ANKLE
101 Mend|Rx Bedford MEND|RX BEDFORD
102 Michael David Williams MICHAEL DAVID  WILLIAMS
103 Michelle Harvey @ Twelfth Avenue Acupuncture and Herb Clinic MICHELLE HARVEY @ TWELFTH AVENUE ACUPUNCTURE AND HERB CLINIC
104 Minister of Finance C/O Ministry of Health MINISTER OF FINANCE C/O MINISTRY OF HEALTH
105 Miracle Ear/Fred Hawkins MIRACLE EAR/FRED HAWKINS
106 Mount Sinai Rehab C/O Mt Sinai Hosp MOUNT SINAI REHAB C/O MT SINAI HOSP
107 Moving Along... Your ! MOVING ALONG... YOUR !
108 Ms Sheila Wolanski/Mid-Island Home Support MS SHEILA WOLANSKI/MID-ISLAND HOME SUPPORT
109 Naomi Anne Ecob NAOMI ANNE  ECOB
110 National Orthotic Centre/# 1703639 NATIONAL ORTHOTIC CENTRE/# 1703639
111 National Orthotic Centre/#1649481 NATIONAL ORTHOTIC CENTRE/#1649481
112 New Sudbury/Val Caron Family Vision Ctre NEW SUDBURY/VAL CARON FAMILY VISION CTRE
113 Noelle Bethea Noelle  Bethea
114 North York General Hospital/Audiology NORTH YORK GENERAL HOSPITAL/AUDIOLOGY
115 Not Just Backs! Chiropractic NOT JUST BACKS! CHIROPRACTIC
116 Oh! Lunettes Par Sardi-Nicopoulos OH! LUNETTES PAR SARDI-NICOPOULOS
117 Omod Kitchen/Guelph/Waterloo OMOD KITCHEN/GUELPH/WATERLOO
118 Ontario Hearing Institute C/O Dorothy Bravo ONTARIO HEARING INSTITUTE C/O DOROTHY BRAVO
119 Optical 20/20 of Whitby OPTICAL 20/20 OF WHITBY
120 Optical 6/6 OPTICAL 6/6
121 Ortho Ml Inc./Respir-O-Max ORTHO ML INC./RESPIR-O-MAX
122 Ossur Canada Inc C/OT44606 OSSUR CANADA INC C/OT44606
123 Ot Consulting/Treatment Services Ltd OT CONSULTING/TREATMENT SERVICES LTD
124 Ottawa Eyelabs Inc C/O Vision Plus OTTAWA EYELABS INC C/O VISION PLUS
125 Ottawa Health: Performance and Rehabilitation OTTAWA HEALTH: PERFORMANCE AND REHABILITATION
126 Pearle Vision 9861 (Orchard Park S/C) PEARLE VISION 9861 (ORCHARD PARK S/C)
127 Pfahl's Drugs/Home Health Care PFAHL'S DRUGS/HOME HEALTH CARE
128 Physio F/X Ltd PHYSIO F/X LTD
129 Physiotherapy Works! PHYSIOTHERAPY WORKS!
130 Pinellas County Ems D/B/A Sunstar PINELLAS COUNTY EMS D/B/A SUNSTAR
131 Pmb/Emergency Medicine of in LLC PMB/EMERGENCY MEDICINE OF IN LLC
132 Professional Hearing Clinic Inc/Connect Hearing PROFESSIONAL HEARING CLINIC INC/CONNECT HEARING
133 Prothotics/Healthwest PROTHOTICS/HEALTHWEST
134 Regents of The U of M U/M Health System REGENTS OF THE U OF M U/M HEALTH SYSTEM
135 Regents of U/M - Medequip REGENTS OF U/M - MEDEQUIP
136 Rehabilitation Network Canada - Guelph/Kitchener/Waterloo REHABILITATION NETWORK CANADA - GUELPH/KITCHENER/WATERLOO
137 Rehabilitation Network Canada - Halton/Peel REHABILITATION NETWORK CANADA - HALTON/PEEL
138 Rehabilitation Network Canada Inc - Halton/Peel REHABILITATION NETWORK CANADA INC - HALTON/PEEL
139 Rehamed Inc O/A Humbertown Physiotherapy REHAMED INC O/A HUMBERTOWN PHYSIOTHERAPY
140 Retire -at- Home Services/North York RETIRE -AT- HOME SERVICES/NORTH YORK
141 Retire-at-Home 0SHAWA/Clarington RETIRE-AT-HOME 0SHAWA/CLARINGTON
142 Richard Kievitz/Hear at Last RICHARD KIEVITZ/HEAR AT LAST
143 Sam Ibraham/Canes Family Hlth Team SAM IBRAHAM/CANES FAMILY HLTH TEAM
144 Sarah Ann Mary Pollock SARAH ANN MARY POLLOCK
145 Sharp Healthcare Pfs/Icd Dept SHARP HEALTHCARE PFS/ICD DEPT
146 Shelburne Medical Drugs O/A Caravaggio Ida SHELBURNE MEDICAL DRUGS O/A CARAVAGGIO IDA
147 Silver Cross O/A 3004773 SILVER CROSS O/A 3004773
148 Six Nations Ltc/Hcc SIX NATIONS LTC/HCC
149 Sole Science Inc/Co St Thomas SOLE SCIENCE INC/CO ST THOMAS
150 Sonago Pharmacy Ltd C/O Main Drug Mart SONAGO PHARMACY LTD C/O MAIN DRUG MART
151 Soul: The Wheelchair Studio SOUL: THE WHEELCHAIR STUDIO
152 Spi Health & Safety Inc C/O Acctng SPI HEALTH & SAFETY INC C/O ACCTNG
153 St Joseph Nuclear Med C/O St Josephs Hlth Ctr ST JOSEPH NUCLEAR MED C/O ST JOSEPHS HLTH CTR
154 St. Meena Pharmacy Ltd O/A College Ctr Pharmacy ST. MEENA PHARMACY LTD O/A COLLEGE CTR PHARMACY
155 Staples / Bd #235 Woodstock STAPLES / BD #235 WOODSTOCK
156 Staples/Home Depot STAPLES/HOME DEPOT
157 Sylvia Pudsey O/A The Renfrew Learning Centre SYLVIA PUDSEY O/A THE RENFREW LEARNING CENTRE
158 Tarit Kumar Kanungo Tarit Kumar  Kanungo
159 The City of Winnipeg/Fire Paramedic Service THE CITY OF WINNIPEG/FIRE PARAMEDIC SERVICE
160 The Hearing Loss Clinic Inc /Sabrina Rhodes THE HEARING LOSS CLINIC INC /SABRINA RHODES
161 The Physio Clinic @ West Durham THE PHYSIO CLINIC @ WEST DURHAM
162 The Rehabilitation Ctre Finance Dept/Accts Rec THE REHABILITATION CTRE FINANCE DEPT/ACCTS REC
163 The Therapy Centre (C/O Dr Mary Cooke) THE THERAPY CENTRE (C/O DR MARY COOKE)
164 Town of Windham C/O Comstar Ambulance Billing Ser TOWN OF WINDHAM C/O COMSTAR AMBULANCE BILLING SER
165 Tram Anh Thi Nguyen TRAM ANH THI  NGUYEN
166 Trillium Health Partners-Qhc Finance/Accts Rec-Cvh TRILLIUM HEALTH PARTNERS-QHC FINANCE/ACCTS REC-CVH
167 Trimble Europe B V C/O T10271C TRIMBLE EUROPE B V C/O T10271C
168 True North Imaging / 1582235 Ontario Ltd. TRUE NORTH IMAGING / 1582235 ONTARIO LTD.
169 Tupley Optical Inc. C/O Aspen Eye Care TUPLEY OPTICAL INC. C/O ASPEN EYE CARE
170 Uwo Staff/Faculty Family Practice Clinic UWO STAFF/FACULTY FAMILY PRACTICE CLINIC
171 Visionworks / Eyes Inpsire VISIONWORKS / EYES INPSIRE
172 Whole>Sum Massage WHOLE>SUM Massage
173 William L. Trenwith / Paul J. Trenwith WILLIAM L. TRENWITH / PAUL J. TRENWITH
174 Wilson Medical Centre/Evans WILSON MEDICAL CENTRE/EVANS
175 Work Fitness Plus C/O Oakville Trafalgar Mh WORK FITNESS PLUS C/O OAKVILLE TRAFALGAR MH
176 Ymca of Simcoe/Muskoka YMCA OF SIMCOE/MUSKOKA
177 Young Drivers of Canada/ Maitland For Lincoln YOUNG DRIVERS OF CANADA/ MAITLAND FOR LINCOLN
178 Zest! Rehabilitation Health & Wellness ZEST! REHABILITATION HEALTH & WELLNESS
179 -Academy of Learning Owen Sound (Priv.) ~ACADEMY OF LEARNING OWEN SOUND (PRIV.)
180 -Appletree Medical Centre ~APPLETREE MEDICAL CENTRE
181 -Brameast Family Physicians ~BRAMEAST FAMILY PHYSICIANS
182 -C. L. Consulting ~C. L. CONSULTING
183 -My Health Centre ~MY HEALTH CENTRE
184 -Upper Canada Hearing & Speech Centre ~UPPER CANADA HEARING & SPEECH CENTRE
185 -Whitby Clinic ~WHITBY CLINIC
186 Sport Physio West Island Inc./Actio Sport Physio West Island Inc./Actio
187 Voir...Être Vu! Opticiens VOIR...ÊTRE VU! OPTICIENS
188 Vpi Inc. - Halton/Peel VPI INC. - HALTON/PEEL

View File

@ -1,3 +1,5 @@
// hapi-fhir/hapi-fhir-structures-r4$ mvn -Dtest=ca.uhn.fhir.parser.RDFParserTest test
package ca.uhn.fhir.parser; package ca.uhn.fhir.parser;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
@ -54,15 +56,6 @@ public class RDFParserTest extends BaseTest {
fhirSchema = GenParser.parseSchema(schemaFile, Collections.emptyList()); fhirSchema = GenParser.parseSchema(schemaFile, Collections.emptyList());
} }
// If we can't round-trip JSON, we skip the Turtle round-trip test.
private static ArrayList<String> jsonRoundTripErrors = new ArrayList<String>();
@AfterAll
static void reportJsonRoundTripErrors() {
System.out.println(jsonRoundTripErrors.size() + " tests disqualified because of JSON round-trip errors");
for (String e : jsonRoundTripErrors)
System.out.println(e);
}
/** /**
* This test method has a method source for each JSON file in the resources/rdf-test-input directory (see #getInputFiles). * This test method has a method source for each JSON file in the resources/rdf-test-input directory (see #getInputFiles).
* Each input file is expected to be a JSON representation of an R4 FHIR resource. * Each input file is expected to be a JSON representation of an R4 FHIR resource.
@ -86,10 +79,6 @@ public class RDFParserTest extends BaseTest {
String turtleString = serializeRdf(ourCtx, referenceResource); String turtleString = serializeRdf(ourCtx, referenceResource);
validateRdf(turtleString, referenceFileName, referenceResource); validateRdf(turtleString, referenceFileName, referenceResource);
// If we can round-trip JSON
IBaseResource viaJsonResource = parseJson(new ByteArrayInputStream(referenceJson.getBytes()));
if (((Base)viaJsonResource).equalsDeep((Base)referenceResource)) {
// Parse RDF content as resource // Parse RDF content as resource
IBaseResource viaTurtleResource = parseRdf(ourCtx, new StringReader(turtleString)); IBaseResource viaTurtleResource = parseRdf(ourCtx, new StringReader(turtleString));
assertNotNull(viaTurtleResource); assertNotNull(viaTurtleResource);
@ -105,16 +94,6 @@ public class RDFParserTest extends BaseTest {
else else
assertEquals(referenceJson, viaTurtleJson, failMessage + "\nttl: " + turtleString); assertEquals(referenceJson, viaTurtleJson, failMessage + "\nttl: " + turtleString);
} }
} else {
String gotString = serializeJson(ourCtx, viaJsonResource);
String skipMessage = referenceFileName + ": failed to round-trip JSON" +
(referenceJson.equals(gotString)
? "\ngot: " + gotString + "\nexp: " + referenceJson
: "\nsome inequality not visible in: " + referenceJson);
System.out.println(referenceFileName + " skipped");
// Specific messages are printed at end of run.
jsonRoundTripErrors.add(skipMessage);
}
} }
private static Stream<String> getInputFiles() throws IOException { private static Stream<String> getInputFiles() throws IOException {

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
});
}
}

View File

@ -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);
});
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}