diff --git a/example-projects/hapi-fhir-base-example-embedded-ws/pom.xml b/example-projects/hapi-fhir-base-example-embedded-ws/pom.xml index c5b8ecc7d7c..ae3a99a8ce8 100644 --- a/example-projects/hapi-fhir-base-example-embedded-ws/pom.xml +++ b/example-projects/hapi-fhir-base-example-embedded-ws/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../../pom.xml diff --git a/example-projects/hapi-fhir-jpaserver-cds-example/pom.xml b/example-projects/hapi-fhir-jpaserver-cds-example/pom.xml index f99289195ff..2f0027d243e 100644 --- a/example-projects/hapi-fhir-jpaserver-cds-example/pom.xml +++ b/example-projects/hapi-fhir-jpaserver-cds-example/pom.xml @@ -10,7 +10,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../../pom.xml diff --git a/example-projects/hapi-fhir-jpaserver-dynamic/pom.xml b/example-projects/hapi-fhir-jpaserver-dynamic/pom.xml index 7a58748bb8b..c49e749a221 100644 --- a/example-projects/hapi-fhir-jpaserver-dynamic/pom.xml +++ b/example-projects/hapi-fhir-jpaserver-dynamic/pom.xml @@ -10,7 +10,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../../pom.xml diff --git a/example-projects/hapi-fhir-jpaserver-example-postgres/pom.xml b/example-projects/hapi-fhir-jpaserver-example-postgres/pom.xml index 214b230b3c8..5ff0d7b9ef0 100644 --- a/example-projects/hapi-fhir-jpaserver-example-postgres/pom.xml +++ b/example-projects/hapi-fhir-jpaserver-example-postgres/pom.xml @@ -10,7 +10,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../../pom.xml diff --git a/example-projects/hapi-fhir-standalone-overlay-example/pom.xml b/example-projects/hapi-fhir-standalone-overlay-example/pom.xml index 0f046a2144e..ce0a7a13ce9 100644 --- a/example-projects/hapi-fhir-standalone-overlay-example/pom.xml +++ b/example-projects/hapi-fhir-standalone-overlay-example/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../../pom.xml hapi-fhir-standalone-overlay-example diff --git a/examples/pom.xml b/examples/pom.xml index 0d93d0d685e..218c17630a5 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 58e0016d889..054dde91604 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index b96efff6431..6f00dafb504 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index b6d417b9cbb..bd368198eff 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java index 7465ee0ddbe..41fddc384e2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeElementCompositeDefinition.java @@ -73,12 +73,12 @@ public abstract class BaseRuntimeElementCompositeDefinition ext private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseRuntimeElementCompositeDefinition.class); private Map forcedOrder = null; - private List myChildren = new ArrayList(); + private List myChildren = new ArrayList<>(); private List myChildrenAndExtensions; private Map, BaseRuntimeElementDefinition> myClassToElementDefinitions; - private FhirContext myContext; - private Map myNameToChild = new HashMap(); - private List myScannedFields = new ArrayList(); + private final FhirContext myContext; + private Map myNameToChild = new HashMap<>(); + private List myScannedFields = new ArrayList<>(); private volatile boolean mySealed; @SuppressWarnings("unchecked") @@ -92,12 +92,12 @@ public abstract class BaseRuntimeElementCompositeDefinition ext * We scan classes for annotated fields in the class but also all of its superclasses */ Class current = theImplementingClass; - LinkedList> classes = new LinkedList>(); + LinkedList> classes = new LinkedList<>(); do { if (forcedOrder == null) { ChildOrder childOrder = current.getAnnotation(ChildOrder.class); if (childOrder != null) { - forcedOrder = new HashMap(); + forcedOrder = new HashMap<>(); for (int i = 0; i < childOrder.names().length; i++) { String nextName = childOrder.names()[i]; if (nextName.endsWith("[x]")) { @@ -115,7 +115,7 @@ public abstract class BaseRuntimeElementCompositeDefinition ext } } while (current != null); - Set fields = new HashSet(); + Set fields = new HashSet<>(); for (Class nextClass : classes) { int fieldIndexInClass = 0; for (Field next : nextClass.getDeclaredFields()) { @@ -192,9 +192,9 @@ public abstract class BaseRuntimeElementCompositeDefinition ext } private void scanCompositeElementForChildren() { - Set elementNames = new HashSet(); - TreeMap orderToElementDef = new TreeMap(); - TreeMap orderToExtensionDef = new TreeMap(); + Set elementNames = new HashSet<>(); + TreeMap orderToElementDef = new TreeMap<>(); + TreeMap orderToExtensionDef = new TreeMap<>(); scanCompositeElementForChildren(elementNames, orderToElementDef, orderToExtensionDef); @@ -203,7 +203,7 @@ public abstract class BaseRuntimeElementCompositeDefinition ext * Find out how many elements don't match any entry in the list * for forced order. Those elements come first. */ - TreeMap newOrderToExtensionDef = new TreeMap(); + TreeMap newOrderToExtensionDef = new TreeMap<>(); int unknownCount = 0; for (BaseRuntimeDeclaredChildDefinition nextEntry : orderToElementDef.values()) { if (!forcedOrder.containsKey(nextEntry.getElementName())) { @@ -220,7 +220,7 @@ public abstract class BaseRuntimeElementCompositeDefinition ext orderToElementDef = newOrderToExtensionDef; } - TreeSet orders = new TreeSet(); + TreeSet orders = new TreeSet<>(); orders.addAll(orderToElementDef.keySet()); orders.addAll(orderToExtensionDef.keySet()); @@ -329,7 +329,7 @@ public abstract class BaseRuntimeElementCompositeDefinition ext * Anything that's marked as unknown is given a new ID that is <0 so that it doesn't conflict with any given IDs and can be figured out later */ if (order == Child.ORDER_UNKNOWN) { - order = Integer.valueOf(0); + order = 0; while (orderMap.containsKey(order)) { order++; } @@ -386,7 +386,7 @@ public abstract class BaseRuntimeElementCompositeDefinition ext /* * Child is a resource reference */ - List> refTypesList = new ArrayList>(); + List> refTypesList = new ArrayList<>(); for (Class nextType : childAnnotation.type()) { if (IBaseReference.class.isAssignableFrom(nextType)) { refTypesList.add(myContext.getVersion().getVersion().isRi() ? IAnyResource.class : IResource.class); @@ -469,10 +469,10 @@ public abstract class BaseRuntimeElementCompositeDefinition ext next.sealAndInitialize(theContext, theClassToElementDefinitions); } - myNameToChild = new HashMap(); + myNameToChild = new HashMap<>(); for (BaseRuntimeChildDefinition next : myChildren) { if (next instanceof RuntimeChildChoiceDefinition) { - String key = ((RuntimeChildChoiceDefinition) next).getElementName()+"[x]"; + String key = next.getElementName()+"[x]"; myNameToChild.put(key, next); } for (String nextName : next.getValidChildNames()) { @@ -486,7 +486,7 @@ public abstract class BaseRuntimeElementCompositeDefinition ext myChildren = Collections.unmodifiableList(myChildren); myNameToChild = Collections.unmodifiableMap(myNameToChild); - List children = new ArrayList(); + List children = new ArrayList<>(); children.addAll(myChildren); /* @@ -554,11 +554,11 @@ public abstract class BaseRuntimeElementCompositeDefinition ext private static class ScannedField { private Child myChildAnnotation; - private List> myChoiceTypes = new ArrayList>(); + private List> myChoiceTypes = new ArrayList<>(); private Class myElementType; private Field myField; private boolean myFirstFieldInNewClass; - public ScannedField(Field theField, Class theClass, boolean theFirstFieldInNewClass) { + ScannedField(Field theField, Class theClass, boolean theFirstFieldInNewClass) { myField = theField; myFirstFieldInNewClass = theFirstFieldInNewClass; @@ -574,10 +574,8 @@ public abstract class BaseRuntimeElementCompositeDefinition ext myChildAnnotation = childAnnotation; myElementType = ModelScanner.determineElementType(theField); - - for (Class nextChoiceType : childAnnotation.type()) { - myChoiceTypes.add(nextChoiceType); - } + + Collections.addAll(myChoiceTypes, childAnnotation.type()); } public Child getChildAnnotation() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ModelScanner.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ModelScanner.java index a40954b9290..24b05464213 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ModelScanner.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/ModelScanner.java @@ -19,17 +19,6 @@ package ca.uhn.fhir.context; * limitations under the License. * #L% */ -import static org.apache.commons.lang3.StringUtils.isBlank; - -import java.io.IOException; -import java.io.InputStream; -import java.lang.annotation.Annotation; -import java.lang.reflect.*; -import java.util.*; -import java.util.Map.Entry; - -import org.apache.commons.io.IOUtils; -import org.hl7.fhir.instance.model.api.*; import ca.uhn.fhir.context.RuntimeSearchParam.RuntimeSearchParamStatusEnum; import ca.uhn.fhir.model.api.*; @@ -38,6 +27,19 @@ import ca.uhn.fhir.model.primitive.BoundCodeDt; import ca.uhn.fhir.model.primitive.XhtmlDt; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.util.ReflectionUtil; +import org.hl7.fhir.instance.model.api.*; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; +import java.util.Map.Entry; + +import static org.apache.commons.lang3.StringUtils.isBlank; class ModelScanner { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ModelScanner.class); @@ -55,7 +57,7 @@ class ModelScanner { private Set> myVersionTypes; ModelScanner(FhirContext theContext, FhirVersionEnum theVersion, Map, BaseRuntimeElementDefinition> theExistingDefinitions, - Collection> theResourceTypes) throws ConfigurationException { + Collection> theResourceTypes) throws ConfigurationException { myContext = theContext; myVersion = theVersion; Set> toScan; @@ -67,32 +69,6 @@ class ModelScanner { init(theExistingDefinitions, toScan); } - static Class determineElementType(Field next) { - Class nextElementType = next.getType(); - if (List.class.equals(nextElementType)) { - nextElementType = ReflectionUtil.getGenericCollectionTypeOfField(next); - } else if (Collection.class.isAssignableFrom(nextElementType)) { - throw new ConfigurationException("Field '" + next.getName() + "' in type '" + next.getClass().getCanonicalName() + "' is a Collection - Only java.util.List curently supported"); - } - return nextElementType; - } - - @SuppressWarnings("unchecked") - static IValueSetEnumBinder> getBoundCodeBinder(Field theNext) { - Class bound = getGenericCollectionTypeOfCodedField(theNext); - if (bound == null) { - throw new ConfigurationException("Field '" + theNext + "' has no parameter for " + BoundCodeDt.class.getSimpleName() + " to determine enum type"); - } - - String fieldName = "VALUESET_BINDER"; - try { - Field bindingField = bound.getField(fieldName); - return (IValueSetEnumBinder>) bindingField.get(null); - } catch (Exception e) { - throw new ConfigurationException("Field '" + theNext + "' has type parameter " + bound.getCanonicalName() + " but this class has no valueset binding field (must have a field called " + fieldName + ")", e); - } - } - public Map, BaseRuntimeElementDefinition> getClassToElementDefinitions() { return myClassToElementDefinitions; } @@ -137,11 +113,7 @@ class ModelScanner { for (Class nextClass : typesToScan) { scan(nextClass); } - for (Iterator> iter = myScanAlso.iterator(); iter.hasNext();) { - if (myClassToElementDefinitions.containsKey(iter.next())) { - iter.remove(); - } - } + myScanAlso.removeIf(theClass -> myClassToElementDefinitions.containsKey(theClass)); typesToScan.clear(); typesToScan.addAll(myScanAlso); myScanAlso.clear(); @@ -152,7 +124,7 @@ class ModelScanner { continue; } BaseRuntimeElementDefinition next = nextEntry.getValue(); - + boolean deferredSeal = false; if (myContext.getPerformanceOptions().contains(PerformanceOptionsEnum.DEFERRED_MODEL_SCANNING)) { if (next instanceof BaseRuntimeElementCompositeDefinition) { @@ -177,16 +149,6 @@ class ModelScanner { return retVal; } - /** - * There are two implementations of all of the annotations (e.g. {@link Child} since the HL7.org ones will eventually replace the HAPI - * ones. Annotations can't extend each other or implement interfaces or anything like that, so rather than duplicate all of the annotation processing code this method just creates an interface - * Proxy to simulate the HAPI annotations if the HL7.org ones are found instead. - */ - static T pullAnnotation(AnnotatedElement theTarget, Class theAnnotationType) { - T retVal = theTarget.getAnnotation(theAnnotationType); - return retVal; - } - private void scan(Class theClass) throws ConfigurationException { BaseRuntimeElementDefinition existingDef = myClassToElementDefinitions.get(theClass); if (existingDef != null) { @@ -197,7 +159,7 @@ class ModelScanner { if (resourceDefinition != null) { if (!IBaseResource.class.isAssignableFrom(theClass)) { throw new ConfigurationException( - "Resource type contains a @" + ResourceDef.class.getSimpleName() + " annotation but does not implement " + IResource.class.getCanonicalName() + ": " + theClass.getCanonicalName()); + "Resource type contains a @" + ResourceDef.class.getSimpleName() + " annotation but does not implement " + IResource.class.getCanonicalName() + ": " + theClass.getCanonicalName()); } @SuppressWarnings("unchecked") Class resClass = (Class) theClass; @@ -212,11 +174,11 @@ class ModelScanner { Class resClass = (Class) theClass; scanCompositeDatatype(resClass, datatypeDefinition); } else if (IPrimitiveType.class.isAssignableFrom(theClass)) { - @SuppressWarnings({ "unchecked" }) + @SuppressWarnings({"unchecked"}) Class> resClass = (Class>) theClass; scanPrimitiveDatatype(resClass, datatypeDefinition); - } - + } + return; } @@ -227,13 +189,13 @@ class ModelScanner { scanBlock(theClass); } else { throw new ConfigurationException( - "Type contains a @" + Block.class.getSimpleName() + " annotation but does not implement " + IResourceBlock.class.getCanonicalName() + ": " + theClass.getCanonicalName()); + "Type contains a @" + Block.class.getSimpleName() + " annotation but does not implement " + IResourceBlock.class.getCanonicalName() + ": " + theClass.getCanonicalName()); } } if (blockDefinition == null //Redundant checking && datatypeDefinition == null && resourceDefinition == null - ) { + ) { throw new ConfigurationException("Resource class[" + theClass.getName() + "] does not contain any valid HAPI-FHIR annotations"); } } @@ -246,7 +208,16 @@ class ModelScanner { throw new ConfigurationException("Block type @" + Block.class.getSimpleName() + " annotation contains no name: " + theClass.getCanonicalName()); } + // Just in case someone messes up when upgrading from DSTU2 + if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { + if (BaseIdentifiableElement.class.isAssignableFrom(theClass)) { + throw new ConfigurationException("@Block class for version " + myContext.getVersion().getVersion().name() + " should not extend " + BaseIdentifiableElement.class.getSimpleName() + ": " + theClass.getName()); + } + } + RuntimeResourceBlockDefinition blockDef = new RuntimeResourceBlockDefinition(resourceName, theClass, isStandardType(theClass), myContext, myClassToElementDefinitions); + blockDef.populateScanAlso(myScanAlso); + myClassToElementDefinitions.put(theClass, blockDef); } @@ -272,14 +243,6 @@ class ModelScanner { elementDef.populateScanAlso(myScanAlso); } - - - static Class> determineEnumTypeForBoundField(Field next) { - @SuppressWarnings("unchecked") - Class> enumType = (Class>) ReflectionUtil.getGenericCollectionTypeOfFieldWithSecondOrderForList(next); - return enumType; - } - private String scanPrimitiveDatatype(Class> theClass, DatatypeDef theDatatypeDefinition) { ourLog.debug("Scanning resource class: {}", theClass.getName()); @@ -333,7 +296,7 @@ class ModelScanner { } if (isBlank(resourceName)) { throw new ConfigurationException("Resource type @" + ResourceDef.class.getSimpleName() + " annotation contains no resource name(): " + theClass.getCanonicalName() - + " - This is only allowed for types that extend other resource types "); + + " - This is only allowed for types that extend other resource types "); } } @@ -345,12 +308,12 @@ class ModelScanner { primaryNameProvider = false; } } - + String resourceId = resourceDefinition.id(); if (!isBlank(resourceId)) { if (myIdToResourceDefinition.containsKey(resourceId)) { throw new ConfigurationException("The following resource types have the same ID of '" + resourceId + "' - " + theClass.getCanonicalName() + " and " - + myIdToResourceDefinition.get(resourceId).getImplementingClass().getCanonicalName()); + + myIdToResourceDefinition.get(resourceId).getImplementingClass().getCanonicalName()); } } @@ -372,7 +335,7 @@ class ModelScanner { * sure that this type gets scanned as well */ resourceDef.populateScanAlso(myScanAlso); - + return resourceName; } @@ -393,7 +356,7 @@ class ModelScanner { } nextClass = nextClass.getSuperclass(); } while (nextClass.equals(Object.class) == false); - + /* * Now scan the fields for search params */ @@ -420,7 +383,7 @@ class ModelScanner { } providesMembershipInCompartments.add(next.name()); } - + if (paramType == RestSearchParameterTypeEnum.COMPOSITE) { compositeFields.put(nextField, searchParam); continue; @@ -442,7 +405,7 @@ class ModelScanner { RuntimeSearchParam param = nameToParam.get(nextName); if (param == null) { ourLog.warn("Search parameter {}.{} declares that it is a composite with compositeOf value '{}' but that is not a valid parametr name itself. Valid values are: {}", - new Object[] { theResourceDef.getName(), searchParam.name(), nextName, nameToParam.keySet() }); + new Object[]{theResourceDef.getName(), searchParam.name(), nextName, nameToParam.keySet()}); continue; } compositeOf.add(param); @@ -455,17 +418,59 @@ class ModelScanner { private Set toTargetList(Class[] theTarget) { HashSet retVal = new HashSet(); - + for (Class nextType : theTarget) { ResourceDef resourceDef = nextType.getAnnotation(ResourceDef.class); if (resourceDef != null) { retVal.add(resourceDef.name()); } } - + return retVal; } + static Class determineElementType(Field next) { + Class nextElementType = next.getType(); + if (List.class.equals(nextElementType)) { + nextElementType = ReflectionUtil.getGenericCollectionTypeOfField(next); + } else if (Collection.class.isAssignableFrom(nextElementType)) { + throw new ConfigurationException("Field '" + next.getName() + "' in type '" + next.getClass().getCanonicalName() + "' is a Collection - Only java.util.List curently supported"); + } + return nextElementType; + } + + @SuppressWarnings("unchecked") + static IValueSetEnumBinder> getBoundCodeBinder(Field theNext) { + Class bound = getGenericCollectionTypeOfCodedField(theNext); + if (bound == null) { + throw new ConfigurationException("Field '" + theNext + "' has no parameter for " + BoundCodeDt.class.getSimpleName() + " to determine enum type"); + } + + String fieldName = "VALUESET_BINDER"; + try { + Field bindingField = bound.getField(fieldName); + return (IValueSetEnumBinder>) bindingField.get(null); + } catch (Exception e) { + throw new ConfigurationException("Field '" + theNext + "' has type parameter " + bound.getCanonicalName() + " but this class has no valueset binding field (must have a field called " + fieldName + ")", e); + } + } + + /** + * There are two implementations of all of the annotations (e.g. {@link Child} since the HL7.org ones will eventually replace the HAPI + * ones. Annotations can't extend each other or implement interfaces or anything like that, so rather than duplicate all of the annotation processing code this method just creates an interface + * Proxy to simulate the HAPI annotations if the HL7.org ones are found instead. + */ + static T pullAnnotation(AnnotatedElement theTarget, Class theAnnotationType) { + T retVal = theTarget.getAnnotation(theAnnotationType); + return retVal; + } + + static Class> determineEnumTypeForBoundField(Field next) { + @SuppressWarnings("unchecked") + Class> enumType = (Class>) ReflectionUtil.getGenericCollectionTypeOfFieldWithSecondOrderForList(next); + return enumType; + } + private static Class getGenericCollectionTypeOfCodedField(Field next) { Class type; ParameterizedType collectionType = (ParameterizedType) next.getGenericType(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildUndeclaredExtensionDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildUndeclaredExtensionDefinition.java index a96f1283ce8..01d81d69855 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildUndeclaredExtensionDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildUndeclaredExtensionDefinition.java @@ -149,9 +149,9 @@ public class RuntimeChildUndeclaredExtensionDefinition extends BaseRuntimeChildD @Override void sealAndInitialize(FhirContext theContext, Map, BaseRuntimeElementDefinition> theClassToElementDefinitions) { - Map> datatypeAttributeNameToDefinition = new HashMap>(); - myDatatypeToAttributeName = new HashMap, String>(); - myDatatypeToDefinition = new HashMap, BaseRuntimeElementDefinition>(); + Map> datatypeAttributeNameToDefinition = new HashMap<>(); + myDatatypeToAttributeName = new HashMap<>(); + myDatatypeToDefinition = new HashMap<>(); for (BaseRuntimeElementDefinition next : theClassToElementDefinitions.values()) { if (next instanceof IRuntimeDatatypeDefinition) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterType.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterType.java index eb57e00164c..b494e020aac 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterType.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/IQueryParameterType.java @@ -60,7 +60,7 @@ public interface IQueryParameterType extends Serializable { public String getValueAsQueryToken(FhirContext theContext); /** - * This method will return any qualifier that should be appended to the parameter name (e.g ":exact") + * This method will return any qualifier that should be appended to the parameter name (e.g ":exact"). Returns null if none are present. */ public String getQueryParameterQualifier(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java index 28c840a0940..16160728112 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.UrlUtil; +import com.google.common.base.Charsets; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.*; @@ -623,6 +624,16 @@ public abstract class BaseParser implements IParser { return mySuppressNarratives; } + @Override + public IBaseResource parseResource(InputStream theInputStream) throws DataFormatException { + return parseResource(new InputStreamReader(theInputStream, Charsets.UTF_8)); + } + + @Override + public T parseResource(Class theResourceType, InputStream theInputStream) throws DataFormatException { + return parseResource(theResourceType, new InputStreamReader(theInputStream, Charsets.UTF_8)); + } + @Override public T parseResource(Class theResourceType, Reader theReader) throws DataFormatException { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java index b10b6e23ea5..0bde9217995 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java @@ -19,14 +19,23 @@ package ca.uhn.fhir.parser; * limitations under the License. * #L% */ -import java.io.*; -import java.util.*; -import org.hl7.fhir.instance.model.api.*; - -import ca.uhn.fhir.context.*; +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.rest.api.EncodingEnum; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Collection; +import java.util.List; +import java.util.Set; /** * A parser, which can be used to convert between HAPI FHIR model/structure objects, and their respective String wire @@ -127,6 +136,20 @@ public interface IParser { */ T parseResource(Class theResourceType, Reader theReader) throws DataFormatException; + /** + * Parses a resource + * + * @param theResourceType + * The resource type to use. This can be used to explicitly specify a class which extends a built-in type + * (e.g. a custom type extending the default Patient class) + * @param theInputStream + * The InputStream to parse input from, with an implied charset of UTF-8. Note that the InputStream will not be closed by the parser upon completion. + * @return A parsed resource + * @throws DataFormatException + * If the resource can not be parsed because the data is not recognized or invalid for any reason + */ + T parseResource(Class theResourceType, InputStream theInputStream) throws DataFormatException; + /** * Parses a resource * @@ -153,6 +176,19 @@ public interface IParser { */ IBaseResource parseResource(Reader theReader) throws ConfigurationException, DataFormatException; + /** + * Parses a resource + * + * @param theInputStream + * The InputStream to parse input from (charset is assumed to be UTF-8). + * Note that the stream will not be closed by the parser upon completion. + * @return A parsed resource. Note that the returned object will be an instance of {@link IResource} or + * {@link IAnyResource} depending on the specific FhirContext which created this parser. + * @throws DataFormatException + * If the resource can not be parsed because the data is not recognized or invalid for any reason + */ + IBaseResource parseResource(InputStream theInputStream) throws ConfigurationException, DataFormatException; + /** * Parses a resource * diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index 4fc0ab5e5a4..deaed08c063 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -42,8 +42,14 @@ public class Constants { */ public static final Set CORS_ALLWED_METHODS; public static final String CT_FHIR_JSON = "application/json+fhir"; + /** + * The FHIR MimeType for JSON encoding in FHIR DSTU3+ + */ public static final String CT_FHIR_JSON_NEW = "application/fhir+json"; public static final String CT_FHIR_XML = "application/xml+fhir"; + /** + * The FHIR MimeType for XML encoding in FHIR DSTU3+ + */ public static final String CT_FHIR_XML_NEW = "application/fhir+xml"; public static final String CT_HTML = "text/html"; public static final String CT_HTML_WITH_UTF8 = "text/html" + CHARSET_UTF8_CTSUFFIX; @@ -86,6 +92,7 @@ public class Constants { public static final String HEADER_CONTENT_LOCATION = "Content-Location"; public static final String HEADER_CONTENT_LOCATION_LC = HEADER_CONTENT_LOCATION.toLowerCase(); public static final String HEADER_CONTENT_TYPE = "Content-Type"; + public static final String HEADER_CONTENT_TYPE_LC = HEADER_CONTENT_TYPE.toLowerCase(); public static final String HEADER_COOKIE = "Cookie"; public static final String HEADER_CORS_ALLOW_METHODS = "Access-Control-Allow-Methods"; public static final String HEADER_CORS_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java index 68ffee11480..fbe11a89491 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/MethodOutcome.java @@ -20,11 +20,13 @@ package ca.uhn.fhir.rest.api; * #L% */ +import ca.uhn.fhir.util.CoverageIgnore; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import ca.uhn.fhir.util.CoverageIgnore; +import java.util.List; +import java.util.Map; public class MethodOutcome { @@ -32,6 +34,7 @@ public class MethodOutcome { private IIdType myId; private IBaseOperationOutcome myOperationOutcome; private IBaseResource myResource; + private Map> myResponseHeaders; /** * Constructor @@ -42,13 +45,10 @@ public class MethodOutcome { /** * Constructor - * - * @param theId - * The ID of the created/updated resource - * - * @param theCreated - * If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called - * whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist. + * + * @param theId The ID of the created/updated resource + * @param theCreated If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called + * whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist. */ @CoverageIgnore public MethodOutcome(IIdType theId, Boolean theCreated) { @@ -58,12 +58,9 @@ public class MethodOutcome { /** * Constructor - * - * @param theId - * The ID of the created/updated resource - * - * @param theBaseOperationOutcome - * The operation outcome to return with the response (or null for none) + * + * @param theId The ID of the created/updated resource + * @param theBaseOperationOutcome The operation outcome to return with the response (or null for none) */ public MethodOutcome(IIdType theId, IBaseOperationOutcome theBaseOperationOutcome) { myId = theId; @@ -72,16 +69,11 @@ public class MethodOutcome { /** * Constructor - * - * @param theId - * The ID of the created/updated resource - * - * @param theBaseOperationOutcome - * The operation outcome to return with the response (or null for none) - * - * @param theCreated - * If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called - * whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist. + * + * @param theId The ID of the created/updated resource + * @param theBaseOperationOutcome The operation outcome to return with the response (or null for none) + * @param theCreated If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called + * whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist. */ public MethodOutcome(IIdType theId, IBaseOperationOutcome theBaseOperationOutcome, Boolean theCreated) { myId = theId; @@ -91,9 +83,8 @@ public class MethodOutcome { /** * Constructor - * - * @param theId - * The ID of the created/updated resource + * + * @param theId The ID of the created/updated resource */ public MethodOutcome(IIdType theId) { myId = theId; @@ -101,9 +92,8 @@ public class MethodOutcome { /** * Constructor - * - * @param theOperationOutcome - * The operation outcome resource to return + * + * @param theOperationOutcome The operation outcome resource to return */ public MethodOutcome(IBaseOperationOutcome theOperationOutcome) { myOperationOutcome = theOperationOutcome; @@ -117,19 +107,54 @@ public class MethodOutcome { return myCreated; } + /** + * If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called whether the + * result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist. + *

+ * Users of HAPI should only interact with this method in Server applications + *

+ * + * @param theCreated If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called + * whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist. + * @return Returns a reference to this for easy method chaining + */ + public MethodOutcome setCreated(Boolean theCreated) { + myCreated = theCreated; + return this; + } + public IIdType getId() { return myId; } + /** + * @param theId The ID of the created/updated resource + * @return Returns a reference to this for easy method chaining + */ + public MethodOutcome setId(IIdType theId) { + myId = theId; + return this; + } + /** * Returns the {@link IBaseOperationOutcome} resource to return to the client or null if none. - * + * * @return This method will return null, unlike many methods in the API. */ public IBaseOperationOutcome getOperationOutcome() { return myOperationOutcome; } + /** + * Sets the {@link IBaseOperationOutcome} resource to return to the client. Set to null (which is the default) if none. + * + * @return Returns a reference to this for easy method chaining + */ + public MethodOutcome setOperationOutcome(IBaseOperationOutcome theBaseOperationOutcome) { + myOperationOutcome = theBaseOperationOutcome; + return this; + } + /** * From a client response: If the method returned an actual resource body (e.g. a create/update with * "Prefer: return=representation") this field will be populated with the @@ -139,50 +164,15 @@ public class MethodOutcome { return myResource; } - /** - * If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called whether the - * result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist. - *

- * Users of HAPI should only interact with this method in Server applications - *

- * - * @param theCreated - * If not null, indicates whether the resource was created (as opposed to being updated). This is generally not needed, since the server can assume based on the method being called - * whether the result was a creation or an update. However, it can be useful if you are implementing an update method that does a create if the ID doesn't already exist. - * @return Returns a reference to this for easy method chaining - */ - public MethodOutcome setCreated(Boolean theCreated) { - myCreated = theCreated; - return this; - } - - /** - * @param theId - * The ID of the created/updated resource - * @return Returns a reference to this for easy method chaining - */ - public MethodOutcome setId(IIdType theId) { - myId = theId; - return this; - } - - /** - * Sets the {@link IBaseOperationOutcome} resource to return to the client. Set to null (which is the default) if none. - * @return Returns a reference to this for easy method chaining - */ - public MethodOutcome setOperationOutcome(IBaseOperationOutcome theBaseOperationOutcome) { - myOperationOutcome = theBaseOperationOutcome; - return this; - } - /** * In a server response: This field may be populated in server code with the final resource for operations * where a resource body is being created/updated. E.g. for an update method, this field could be populated with - * the resource after the update is applied, with the new version ID, lastUpdate time, etc. + * the resource after the update is applied, with the new version ID, lastUpdate time, etc. *

* This field is optional, but if it is populated the server will return the resource body if requested to * do so via the HTTP Prefer header. - *

+ *

+ * * @return Returns a reference to this for easy method chaining */ public MethodOutcome setResource(IBaseResource theResource) { @@ -190,4 +180,23 @@ public class MethodOutcome { return this; } + /** + * Gets the headers for the HTTP response + */ + public Map> getResponseHeaders() { + return myResponseHeaders; + } + + /** + * Sets the headers for the HTTP response + */ + public void setResponseHeaders(Map> theResponseHeaders) { + myResponseHeaders = theResponseHeaders; + } + + public void setCreatedUsingStatusCode(int theResponseStatusCode) { + if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) { + setCreated(true); + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpRequest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpRequest.java index d7780caa397..697215164de 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpRequest.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpRequest.java @@ -28,16 +28,18 @@ import java.util.Map; * Http Request. Allows addition of headers and execution of the request. */ public interface IHttpRequest { - + /** * Add a header to the request - * @param theName the header name + * + * @param theName the header name * @param theValue the header value */ void addHeader(String theName, String theValue); /** * Execute the request + * * @return the response */ IHttpResponse execute() throws IOException; @@ -50,7 +52,8 @@ public interface IHttpRequest { /** * Return the request body as a string. - * If this is not supported by the underlying technology, null is returned + * If this is not supported by the underlying technology, null is returned + * * @return a string representation of the request or null if not supported or empty. */ String getRequestBodyFromStream() throws IOException; @@ -59,10 +62,16 @@ public interface IHttpRequest { * Return the request URI, or null */ String getUri(); - + /** * Return the HTTP verb (e.g. "GET") */ String getHttpVerbName(); - + + /** + * Remove any headers matching the given name + * + * @param theHeaderName The header name, e.g. "Accept" (must not be null or blank) + */ + void removeHeaders(String theHeaderName); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpResponse.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpResponse.java index 9c5a6f395e6..4e84c9f0617 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpResponse.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IHttpResponse.java @@ -70,7 +70,7 @@ public interface IHttpResponse { void close(); /** - * Returna reader for the response entity + * Returns a reader for the response entity */ Reader createReader() throws IOException; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/exceptions/NonFhirResponseException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/exceptions/NonFhirResponseException.java index 5cf326bb9f6..763dcaa9cc4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/exceptions/NonFhirResponseException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/exceptions/NonFhirResponseException.java @@ -19,15 +19,18 @@ package ca.uhn.fhir.rest.client.exceptions; * limitations under the License. * #L% */ -import static org.apache.commons.lang3.StringUtils.isBlank; - -import java.io.IOException; -import java.io.Reader; - -import org.apache.commons.io.IOUtils; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.util.CoverageIgnore; +import com.google.common.base.Charsets; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; + +import static org.apache.commons.lang3.StringUtils.isBlank; @CoverageIgnore public class NonFhirResponseException extends BaseServerResponseException { @@ -36,24 +39,30 @@ public class NonFhirResponseException extends BaseServerResponseException { /** * Constructor - * - * @param theMessage - * The message - * @param theResponseText - * @param theStatusCode - * @param theResponseReader - * @param theContentType + * + * @param theMessage The message + * @param theStatusCode The HTTP status code */ NonFhirResponseException(int theStatusCode, String theMessage) { super(theStatusCode, theMessage); } + public static NonFhirResponseException newInstance(int theStatusCode, String theContentType, InputStream theInputStream) { + return newInstance(theStatusCode, theContentType, new InputStreamReader(theInputStream, Charsets.UTF_8)); + } + public static NonFhirResponseException newInstance(int theStatusCode, String theContentType, Reader theReader) { String responseBody = ""; try { responseBody = IOUtils.toString(theReader); } catch (IOException e) { - IOUtils.closeQuietly(theReader); + // ignore + } finally { + try { + theReader.close(); + } catch (IOException theE) { + // ignore + } } NonFhirResponseException retVal; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java index 331575d2605..ffe305acfad 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IClientExecutable.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.gclient; import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum; import ca.uhn.fhir.rest.api.SummaryEnum; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -28,12 +29,12 @@ import java.util.List; */ -public interface IClientExecutable, Y> { +public interface IClientExecutable, Y> { /** * If set to true, the client will log the request and response to the SLF4J logger. This can be useful for * debugging, but is generally not desirable in a production situation. - * + * * @deprecated Use the client logging interceptor to log requests and responses instead. See here for more information. */ @Deprecated @@ -46,16 +47,45 @@ public interface IClientExecutable, Y> { T cacheControl(CacheControlDirective theCacheControlDirective); /** - * Request that the server return subsetted resources, containing only the elements specified in the given parameters. + * Request that the server return subsetted resources, containing only the elements specified in the given parameters. * For example: subsetElements("name", "identifier") requests that the server only return - * the "name" and "identifier" fields in the returned resource, and omit any others. + * the "name" and "identifier" fields in the returned resource, and omit any others. */ T elementsSubset(String... theElements); + /** + * Request that the server respond with JSON via the Accept header and possibly also the + * _format parameter if {@link ca.uhn.fhir.rest.client.api.IRestfulClient#setFormatParamStyle(RequestFormatParamStyleEnum) configured to do so}. + *

+ * This method will have no effect if {@link #accept(String) a custom Accept header} is specified. + *

+ * + * @see #accept(String) + */ T encoded(EncodingEnum theEncoding); + /** + * Request that the server respond with JSON via the Accept header and possibly also the + * _format parameter if {@link ca.uhn.fhir.rest.client.api.IRestfulClient#setFormatParamStyle(RequestFormatParamStyleEnum) configured to do so}. + *

+ * This method will have no effect if {@link #accept(String) a custom Accept header} is specified. + *

+ * + * @see #accept(String) + * @see #encoded(EncodingEnum) + */ T encodedJson(); + /** + * Request that the server respond with JSON via the Accept header and possibly also the + * _format parameter if {@link ca.uhn.fhir.rest.client.api.IRestfulClient#setFormatParamStyle(RequestFormatParamStyleEnum) configured to do so}. + *

+ * This method will have no effect if {@link #accept(String) a custom Accept header} is specified. + *

+ * + * @see #accept(String) + * @see #encoded(EncodingEnum) + */ T encodedXml(); /** @@ -84,11 +114,33 @@ public interface IClientExecutable, Y> { */ T preferResponseTypes(List> theTypes); + /** + * Request pretty-printed response via the _pretty parameter + */ T prettyPrint(); /** - * Request that the server modify the response using the _summary param + * Request that the server modify the response using the _summary param */ T summaryMode(SummaryEnum theSummary); + /** + * Specifies a custom Accept header that should be supplied with the + * request. + *

+ * Note that this method overrides any encoding preferences specified with + * {@link #encodedJson()} or {@link #encodedXml()}. It is generally easier to + * just use those methods if you simply want to request a specific FHIR encoding. + *

+ * + * @param theHeaderValue The header value, e.g. "application/fhir+json". Constants such + * as {@link ca.uhn.fhir.rest.api.Constants#CT_FHIR_XML_NEW} and + * {@link ca.uhn.fhir.rest.api.Constants#CT_FHIR_JSON_NEW} may + * be useful. If set to null or an empty string, the + * default Accept header will be used. + * @see #encoded(EncodingEnum) + * @see #encodedJson() + * @see #encodedXml() + */ + T accept(String theHeaderValue); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInput.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInput.java index ba922755475..ea41945171b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInput.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInput.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.gclient; * #L% */ +import ca.uhn.fhir.rest.api.MethodOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; public interface IOperationUntypedWithInput extends IClientExecutable, T> { @@ -43,4 +44,9 @@ public interface IOperationUntypedWithInput extends IClientExecutable IOperationUntypedWithInput returnResourceType(Class theReturnType); + /** + * Request that the method chain returns a {@link MethodOutcome} object. This object + * will contain details + */ + IOperationUntypedWithInput returnMethodOutcome(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java index 669ebe2a57a..d25afa5340d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java @@ -100,7 +100,11 @@ public class TokenParam extends BaseParam /*implements IQueryParameterType*/ { @Override String doGetValueAsQueryToken(FhirContext theContext) { if (getSystem() != null) { - return ParameterUtil.escape(StringUtils.defaultString(getSystem())) + '|' + ParameterUtil.escape(getValue()); + if (getValue() != null) { + return ParameterUtil.escape(StringUtils.defaultString(getSystem())) + '|' + ParameterUtil.escape(getValue()); + } else { + return ParameterUtil.escape(StringUtils.defaultString(getSystem())) + '|'; + } } return ParameterUtil.escape(getValue()); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java index a7fa566afa8..73fd6e72385 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java @@ -25,6 +25,7 @@ public enum VersionEnum { V3_3_0, V3_4_0, V3_5_0, - V3_6_0 + V3_6_0, + V3_7_0 } diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 14064dda16c..1ffeda550f2 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../../hapi-deployable-pom/pom.xml @@ -188,7 +188,7 @@ org.thymeleaf - thymeleaf-spring4 + thymeleaf-spring5 diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index b4310b6332e..83ee05de03a 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml @@ -43,6 +43,14 @@ classes + + diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index cbcd59063a5..ed09890f69c 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 7392f0df5ab..5bf001beef5 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 30d391c91d3..08842064b96 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulRequest.java b/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulRequest.java index 5ad07eceb2d..e8569f9c755 100644 --- a/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulRequest.java +++ b/hapi-fhir-client-okhttp/src/main/java/ca/uhn/fhir/okhttp/client/OkHttpRestfulRequest.java @@ -94,4 +94,9 @@ public class OkHttpRestfulRequest implements IHttpRequest { return myRequestTypeEnum.name(); } + @Override + public void removeHeaders(String theHeaderName) { + myRequestBuilder.removeHeader(theHeaderName); + } + } diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index 07fab225886..052fc0b3208 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpRequest.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpRequest.java index f968eb8901b..895c2a526fa 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpRequest.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpRequest.java @@ -20,12 +20,11 @@ package ca.uhn.fhir.rest.client.apache; * #L% */ -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.*; - +import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.IHttpResponse; import ca.uhn.fhir.util.StopWatch; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; @@ -34,13 +33,14 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.ContentType; -import ca.uhn.fhir.rest.client.api.IHttpRequest; -import ca.uhn.fhir.rest.client.api.IHttpResponse; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.*; /** * A Http Request based on Apache. This is an adapter around the class * {@link org.apache.http.client.methods.HttpRequestBase HttpRequestBase} - * + * * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare */ public class ApacheHttpRequest implements IHttpRequest { @@ -79,6 +79,7 @@ public class ApacheHttpRequest implements IHttpRequest { /** * Get the ApacheRequest + * * @return the ApacheRequest */ public HttpRequestBase getApacheRequest() { @@ -90,6 +91,12 @@ public class ApacheHttpRequest implements IHttpRequest { return myRequest.getMethod(); } + @Override + public void removeHeaders(String theHeaderName) { + Validate.notBlank(theHeaderName, "theHeaderName must not be null or blank"); + myRequest.removeHeaders(theHeaderName); + } + @Override public String getRequestBodyFromStream() throws IOException { if (myRequest instanceof HttpEntityEnclosingRequest) { diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java index e81a7486170..d130179a6d0 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java @@ -33,19 +33,20 @@ import ca.uhn.fhir.rest.client.method.IClientResponseHandler; import ca.uhn.fhir.rest.client.method.IClientResponseHandlerHandlesBinary; import ca.uhn.fhir.rest.client.method.MethodUtil; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.BinaryUtil; import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.XmlDetectionUtil; +import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.*; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; +import java.io.*; import java.util.*; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseClient implements IRestfulClient { @@ -93,14 +94,16 @@ public abstract class BaseClient implements IRestfulClient { } - protected Map> createExtraParams() { - HashMap> retVal = new LinkedHashMap>(); + protected Map> createExtraParams(String theCustomAcceptHeader) { + HashMap> retVal = new LinkedHashMap<>(); - if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) { - if (getEncoding() == EncodingEnum.XML) { - retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); - } else if (getEncoding() == EncodingEnum.JSON) { - retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); + if (isBlank(theCustomAcceptHeader)) { + if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) { + if (getEncoding() == EncodingEnum.XML) { + retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); + } else if (getEncoding() == EncodingEnum.JSON) { + retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); + } } } @@ -115,7 +118,7 @@ public abstract class BaseClient implements IRestfulClient { public T fetchResourceFromUrl(Class theResourceType, String theUrl) { BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl); ResourceResponseHandler binding = new ResourceResponseHandler(theResourceType); - return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null); + return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null, null); } void forceConformanceCheck() { @@ -200,11 +203,11 @@ public abstract class BaseClient implements IRestfulClient { } T invokeClient(FhirContext theContext, IClientResponseHandler binding, BaseHttpClientInvocation clientInvocation, boolean theLogRequestAndResponse) { - return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null); + return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null, null); } T invokeClient(FhirContext theContext, IClientResponseHandler binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint, - boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set theSubsetElements, CacheControlDirective theCacheControlDirective) { + boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set theSubsetElements, CacheControlDirective theCacheControlDirective, String theCustomAcceptHeader) { if (!myDontValidateConformance) { myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this); @@ -215,10 +218,10 @@ public abstract class BaseClient implements IRestfulClient { IHttpRequest httpRequest = null; IHttpResponse response = null; try { - Map> params = createExtraParams(); + Map> params = createExtraParams(theCustomAcceptHeader); if (clientInvocation instanceof HttpGetClientInvocation) { - if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) { + if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT && isBlank(theCustomAcceptHeader)) { if (theEncoding == EncodingEnum.XML) { params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); } else if (theEncoding == EncodingEnum.JSON) { @@ -248,6 +251,11 @@ public abstract class BaseClient implements IRestfulClient { httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding, thePrettyPrint); + if (isNotBlank(theCustomAcceptHeader)) { + httpRequest.removeHeaders(Constants.HEADER_ACCEPT); + httpRequest.addHeader(Constants.HEADER_ACCEPT, theCustomAcceptHeader); + } + if (theCacheControlDirective != null) { StringBuilder b = new StringBuilder(); addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_CACHE, theCacheControlDirective.isNoCache()); @@ -289,14 +297,10 @@ public abstract class BaseClient implements IRestfulClient { if (response.getStatus() < 200 || response.getStatus() > 299) { String body = null; - Reader reader = null; - try { - reader = response.createReader(); + try (Reader reader = response.createReader()) { body = IOUtils.toString(reader); } catch (Exception e) { ourLog.debug("Failed to read input stream", e); - } finally { - IOUtils.closeQuietly(reader); } String message = "HTTP " + response.getStatus() + " " + response.getStatusInfo(); @@ -334,27 +338,24 @@ public abstract class BaseClient implements IRestfulClient { if (binding instanceof IClientResponseHandlerHandlesBinary) { IClientResponseHandlerHandlesBinary handlesBinary = (IClientResponseHandlerHandlesBinary) binding; if (handlesBinary.isBinary()) { - InputStream reader = response.readEntity(); - try { - return handlesBinary.invokeClient(mimeType, reader, response.getStatus(), headers); - } finally { - IOUtils.closeQuietly(reader); + try (InputStream reader = response.readEntity()) { + return handlesBinary.invokeClientForBinary(mimeType, reader, response.getStatus(), headers); } } } - Reader reader = response.createReader(); + try (InputStream inputStream = response.readEntity()) { + InputStream inputStreamToReturn = inputStream; - if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) { - String responseString = IOUtils.toString(reader); - keepResponseAndLogIt(theLogRequestAndResponse, response, responseString); - reader = new StringReader(responseString); - } + if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) { + if (inputStream != null) { + String responseString = IOUtils.toString(inputStream, Charsets.UTF_8); + keepResponseAndLogIt(theLogRequestAndResponse, response, responseString); + inputStreamToReturn = new ByteArrayInputStream(responseString.getBytes(Charsets.UTF_8)); + } + } - try { - return binding.invokeClient(mimeType, reader, response.getStatus(), headers); - } finally { - IOUtils.closeQuietly(reader); + return binding.invokeClient(mimeType, inputStreamToReturn, response.getStatus(), headers); } } catch (DataFormatException e) { @@ -463,7 +464,48 @@ public abstract class BaseClient implements IRestfulClient { myInterceptors.remove(theInterceptor); } - protected final class ResourceResponseHandler implements IClientResponseHandler { + protected final class ResourceOrBinaryResponseHandler extends ResourceResponseHandler { + + + @Override + public IBaseResource invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { + + /* + * For operation responses, if the response content type is a FHIR content-type + * (which is will probably almost always be) we just handle it normally. However, + * if we get back a successful (2xx) response from an operation, and the content + * type is something other than FHIR, we'll return it as a Binary wrapped in + * a Parameters resource. + */ + EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); + if (respType != null || theResponseStatusCode < 200 || theResponseStatusCode >= 300) { + return super.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders); + } + + // Create a Binary resource to return + IBaseBinary responseBinary = BinaryUtil.newBinary(getFhirContext()); + + // Fetch the content type + String contentType = null; + List contentTypeHeaders = theHeaders.get(Constants.HEADER_CONTENT_TYPE_LC); + if (contentTypeHeaders != null && contentTypeHeaders.size() > 0) { + contentType = contentTypeHeaders.get(0); + } + responseBinary.setContentType(contentType); + + // Fetch the content itself + try { + responseBinary.setContent(IOUtils.toByteArray(theResponseInputStream)); + } catch (IOException e) { + throw new InternalErrorException("IO failure parsing response", e); + } + + return responseBinary; + } + + } + + protected class ResourceResponseHandler implements IClientResponseHandler { private boolean myAllowHtmlResponse; private IIdType myId; @@ -498,20 +540,20 @@ public abstract class BaseClient implements IRestfulClient { } @Override - public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { + public T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { if (myAllowHtmlResponse && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) && myReturnType != null) { - return readHtmlResponse(theResponseReader); + return readHtmlResponse(theResponseInputStream); } - throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); + throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream); } IParser parser = respType.newParser(getFhirContext()); parser.setServerBaseUrl(getUrlBase()); if (myPreferResponseTypes != null) { parser.setPreferTypes(myPreferResponseTypes); } - T retVal = parser.parseResource(myReturnType, theResponseReader); + T retVal = parser.parseResource(myReturnType, theResponseInputStream); MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal); @@ -519,7 +561,7 @@ public abstract class BaseClient implements IRestfulClient { } @SuppressWarnings("unchecked") - private T readHtmlResponse(Reader theResponseReader) { + private T readHtmlResponse(InputStream theResponseInputStream) { RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myReturnType); IBaseResource instance = resDef.newInstance(); BaseRuntimeChildDefinition textChild = resDef.getChildByName("text"); @@ -531,7 +573,7 @@ public abstract class BaseClient implements IRestfulClient { BaseRuntimeElementDefinition divElement = divChild.getChildByName("div"); IPrimitiveType divInstance = (IPrimitiveType) divElement.newInstance(); try { - divInstance.setValueAsString(IOUtils.toString(theResponseReader)); + divInstance.setValueAsString(IOUtils.toString(theResponseInputStream, Charsets.UTF_8)); } catch (Exception e) { throw new InvalidResponseException(400, "Failed to process HTML response from server: " + e.getMessage(), e); } @@ -539,8 +581,9 @@ public abstract class BaseClient implements IRestfulClient { return (T) instance; } - public void setPreferResponseTypes(List> thePreferResponseTypes) { + public ResourceResponseHandler setPreferResponseTypes(List> thePreferResponseTypes) { myPreferResponseTypes = thePreferResponseTypes; + return this; } } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java index b9705fc3534..175efe5c7ba 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java @@ -46,19 +46,19 @@ import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; import ca.uhn.fhir.util.ICallable; import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.UrlUtil; +import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.*; import java.io.IOException; +import java.io.InputStream; import java.io.Reader; import java.util.*; import java.util.Map.Entry; -import static org.apache.commons.lang3.StringUtils.defaultString; -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.*; /** * @author James Agnew @@ -98,7 +98,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } private T doReadOrVRead(final Class theType, IIdType theId, boolean theVRead, ICallable theNotModifiedHandler, String theIfVersionMatches, Boolean thePrettyPrint, - SummaryEnum theSummary, EncodingEnum theEncoding, Set theSubsetElements) { + SummaryEnum theSummary, EncodingEnum theEncoding, Set theSubsetElements, String theCustomAcceptHeaderValue) { String resName = toResourceName(theType); IIdType id = theId; if (!id.hasBaseUrl()) { @@ -120,7 +120,7 @@ public class GenericClient extends BaseClient implements IGenericClient { } } if (isKeepResponses()) { - myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint()); + myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(theCustomAcceptHeaderValue), getEncoding(), isPrettyPrint()); } if (theIfVersionMatches != null) { @@ -131,10 +131,10 @@ public class GenericClient extends BaseClient implements IGenericClient { ResourceResponseHandler binding = new ResourceResponseHandler<>(theType, (Class) null, id, allowHtmlResponse); if (theNotModifiedHandler == null) { - return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null); + return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null, theCustomAcceptHeaderValue); } try { - return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null); + return invokeClient(myContext, binding, invocation, theEncoding, thePrettyPrint, myLogRequestAndResponse, theSummary, theSubsetElements, null, theCustomAcceptHeaderValue); } catch (NotModifiedException e) { return theNotModifiedHandler.call(); } @@ -228,7 +228,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public T read(final Class theType, UriDt theUrl) { IdDt id = theUrl instanceof IdDt ? ((IdDt) theUrl) : new IdDt(theUrl); - return doReadOrVRead(theType, id, false, null, null, false, null, null, null); + return doReadOrVRead(theType, id, false, null, null, false, null, null, null, null); } @Override @@ -269,7 +269,7 @@ public class GenericClient extends BaseClient implements IGenericClient { public MethodOutcome update(IdDt theIdDt, IBaseResource theResource) { BaseHttpClientInvocation invocation = MethodUtil.createUpdateInvocation(theResource, null, theIdDt, myContext); if (isKeepResponses()) { - myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint()); + myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(null), getEncoding(), isPrettyPrint()); } OutcomeResponseHandler binding = new OutcomeResponseHandler(); @@ -293,7 +293,7 @@ public class GenericClient extends BaseClient implements IGenericClient { invocation = ValidateMethodBindingDstu2Plus.createValidateInvocation(myContext, theResource); if (isKeepResponses()) { - myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint()); + myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(null), getEncoding(), isPrettyPrint()); } OutcomeResponseHandler binding = new OutcomeResponseHandler(); @@ -306,7 +306,7 @@ public class GenericClient extends BaseClient implements IGenericClient { if (theId.hasVersionIdPart() == false) { throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_NO_VERSION_ID_FOR_VREAD, theId.getValue())); } - return doReadOrVRead(theType, theId, true, null, null, false, null, null, null); + return doReadOrVRead(theType, theId, true, null, null, false, null, null, null, null); } @Override @@ -315,49 +315,6 @@ public class GenericClient extends BaseClient implements IGenericClient { return vread(theType, resId); } - private static void addParam(Map> params, String parameterName, String parameterValue) { - if (!params.containsKey(parameterName)) { - params.put(parameterName, new ArrayList<>()); - } - params.get(parameterName).add(parameterValue); - } - - private static void addPreferHeader(PreferReturnEnum thePrefer, BaseHttpClientInvocation theInvocation) { - if (thePrefer != null) { - theInvocation.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + thePrefer.getHeaderValue()); - } - } - - private static String validateAndEscapeConditionalUrl(String theSearchUrl) { - Validate.notBlank(theSearchUrl, "Conditional URL can not be blank/null"); - StringBuilder b = new StringBuilder(); - boolean haveHadQuestionMark = false; - for (int i = 0; i < theSearchUrl.length(); i++) { - char nextChar = theSearchUrl.charAt(i); - if (!haveHadQuestionMark) { - if (nextChar == '?') { - haveHadQuestionMark = true; - } else if (!Character.isLetter(nextChar)) { - throw new IllegalArgumentException("Conditional URL must be in the format \"[ResourceType]?[Params]\" and must not have a base URL - Found: " + theSearchUrl); - } - b.append(nextChar); - } else { - switch (nextChar) { - case '|': - case '?': - case '$': - case ':': - b.append(UrlUtil.escapeUrlParam(Character.toString(nextChar))); - break; - default: - b.append(nextChar); - break; - } - } - } - return b.toString(); - } - private enum MetaOperation { ADD, DELETE, @@ -366,14 +323,25 @@ public class GenericClient extends BaseClient implements IGenericClient { private abstract class BaseClientExecutable, Y> implements IClientExecutable { - protected EncodingEnum myParamEncoding; - protected Boolean myPrettyPrint; - protected SummaryEnum mySummaryMode; - protected CacheControlDirective myCacheControlDirective; + EncodingEnum myParamEncoding; + Boolean myPrettyPrint; + SummaryEnum mySummaryMode; + CacheControlDirective myCacheControlDirective; + private String myCustomAcceptHeaderValue; private List> myPreferResponseTypes; private boolean myQueryLogRequestAndResponse; private HashSet mySubsetElements; + public String getCustomAcceptHeaderValue() { + return myCustomAcceptHeaderValue; + } + + @Override + public T accept(String theHeaderValue) { + myCustomAcceptHeaderValue = theHeaderValue; + return (T) this; + } + @Deprecated // override deprecated method @SuppressWarnings("unchecked") @Override @@ -392,7 +360,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public T elementsSubset(String... theElements) { if (theElements != null && theElements.length > 0) { - mySubsetElements = new HashSet(Arrays.asList(theElements)); + mySubsetElements = new HashSet<>(Arrays.asList(theElements)); } else { mySubsetElements = null; } @@ -444,7 +412,7 @@ public class GenericClient extends BaseClient implements IGenericClient { myLastRequest = theInvocation.asHttpRequest(getServerBase(), theParams, getEncoding(), myPrettyPrint); } - Z resp = invokeClient(myContext, theHandler, theInvocation, myParamEncoding, myPrettyPrint, myQueryLogRequestAndResponse || myLogRequestAndResponse, mySummaryMode, mySubsetElements, myCacheControlDirective); + Z resp = invokeClient(myContext, theHandler, theInvocation, myParamEncoding, myPrettyPrint, myQueryLogRequestAndResponse || myLogRequestAndResponse, mySummaryMode, mySubsetElements, myCacheControlDirective, myCustomAcceptHeaderValue); return resp; } @@ -461,7 +429,7 @@ public class GenericClient extends BaseClient implements IGenericClient { public T preferResponseType(Class theClass) { myPreferResponseTypes = null; if (theClass != null) { - myPreferResponseTypes = new ArrayList>(); + myPreferResponseTypes = new ArrayList<>(); myPreferResponseTypes.add(theClass); } return (T) this; @@ -1021,14 +989,14 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings("unchecked") @Override - public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { + public T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { - throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); + throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream); } IParser parser = respType.newParser(myContext); RuntimeResourceDefinition type = myContext.getResourceDefinition("Parameters"); - IBaseResource retVal = parser.parseResource(type.getImplementingClass(), theResponseReader); + IBaseResource retVal = parser.parseResource(type.getImplementingClass(), theResponseInputStream); BaseRuntimeChildDefinition paramChild = type.getChildByName("parameter"); BaseRuntimeElementCompositeDefinition paramChildElem = (BaseRuntimeElementCompositeDefinition) paramChild.getChildByName("parameter"); @@ -1061,6 +1029,7 @@ public class GenericClient extends BaseClient implements IGenericClient { private Class myReturnResourceType; private Class myType; private boolean myUseHttpGet; + private boolean myReturnMethodOutcome; @SuppressWarnings("unchecked") private void addParam(String theName, IBase theValue) { @@ -1170,11 +1139,19 @@ public class GenericClient extends BaseClient implements IGenericClient { Object retVal = invoke(null, handler, invocation); return retVal; } - ResourceResponseHandler handler; - handler = new ResourceResponseHandler(); - handler.setPreferResponseTypes(getPreferResponseTypes(myType)); + IClientResponseHandler handler = new ResourceOrBinaryResponseHandler() + .setPreferResponseTypes(getPreferResponseTypes(myType)); + + if (myReturnMethodOutcome) { + handler = new MethodOutcomeResponseHandler(handler); + } Object retVal = invoke(null, handler, invocation); + + if (myReturnMethodOutcome) { + return retVal; + } + if (myContext.getResourceDefinition((IBaseResource) retVal).getName().equals("Parameters")) { return retVal; } @@ -1236,6 +1213,12 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } + @Override + public IOperationUntypedWithInput returnMethodOutcome() { + myReturnMethodOutcome = true; + return this; + } + @SuppressWarnings("unchecked") @Override public IOperationProcessMsgMode setMessageBundle(IBaseBundle theMsgBundle) { @@ -1331,10 +1314,30 @@ public class GenericClient extends BaseClient implements IGenericClient { } + + private final class MethodOutcomeResponseHandler implements IClientResponseHandler { + private final IClientResponseHandler myWrap; + + private MethodOutcomeResponseHandler(IClientResponseHandler theWrap) { + myWrap = theWrap; + } + + @Override + public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { + IBaseResource response = myWrap.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders); + + MethodOutcome retVal = new MethodOutcome(); + retVal.setResource(response); + retVal.setCreatedUsingStatusCode(theResponseStatusCode); + retVal.setResponseHeaders(theHeaders); + return retVal; + } + } + private final class OperationOutcomeResponseHandler implements IClientResponseHandler { @Override - public IBaseOperationOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) + public IBaseOperationOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { @@ -1344,7 +1347,7 @@ public class GenericClient extends BaseClient implements IGenericClient { IBaseOperationOutcome retVal; try { // TODO: handle if something else than OO comes back - retVal = (IBaseOperationOutcome) parser.parseResource(theResponseReader); + retVal = (IBaseOperationOutcome) parser.parseResource(theResponseInputStream); } catch (DataFormatException e) { ourLog.warn("Failed to parse OperationOutcome response", e); return null; @@ -1368,11 +1371,9 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { - MethodOutcome response = MethodUtil.process2xxResponse(myContext, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders); - if (theResponseStatusCode == Constants.STATUS_HTTP_201_CREATED) { - response.setCreated(true); - } + public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { + MethodOutcome response = MethodUtil.process2xxResponse(myContext, theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders); + response.setCreatedUsingStatusCode(theResponseStatusCode); if (myPrefer == PreferReturnEnum.REPRESENTATION) { if (response.getResource() == null) { @@ -1384,6 +1385,8 @@ public class GenericClient extends BaseClient implements IGenericClient { } } + response.setResponseHeaders(theHeaders); + return response; } } @@ -1511,9 +1514,9 @@ public class GenericClient extends BaseClient implements IGenericClient { @Override public Object execute() {// AAA if (myId.hasVersionIdPart()) { - return doReadOrVRead(myType.getImplementingClass(), myId, true, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements()); + return doReadOrVRead(myType.getImplementingClass(), myId, true, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue()); } - return doReadOrVRead(myType.getImplementingClass(), myId, false, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements()); + return doReadOrVRead(myType.getImplementingClass(), myId, false, myNotModifiedHandler, myIfVersionMatches, myPrettyPrint, mySummaryMode, myParamEncoding, getSubsetElements(), getCustomAcceptHeaderValue()); } @Override @@ -1636,11 +1639,11 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings("unchecked") @Override - public List invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) + public List invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { Class bundleType = myContext.getResourceDefinition("Bundle").getImplementingClass(); - ResourceResponseHandler handler = new ResourceResponseHandler((Class) bundleType); - IBaseResource response = handler.invokeClient(theResponseMimeType, theResponseReader, theResponseStatusCode, theHeaders); + ResourceResponseHandler handler = new ResourceResponseHandler<>((Class) bundleType); + IBaseResource response = handler.invokeClient(theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders); IVersionSpecificBundleFactory bundleFactory = myContext.newBundleFactory(); bundleFactory.initializeWithBundleResource(response); return bundleFactory.toListOfResources(); @@ -1937,9 +1940,9 @@ public class GenericClient extends BaseClient implements IGenericClient { private final class StringResponseHandler implements IClientResponseHandler { @Override - public String invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) + public String invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { - return IOUtils.toString(theResponseReader); + return IOUtils.toString(theResponseInputStream, Charsets.UTF_8); } } @@ -2251,4 +2254,47 @@ public class GenericClient extends BaseClient implements IGenericClient { } + private static void addParam(Map> params, String parameterName, String parameterValue) { + if (!params.containsKey(parameterName)) { + params.put(parameterName, new ArrayList<>()); + } + params.get(parameterName).add(parameterValue); + } + + private static void addPreferHeader(PreferReturnEnum thePrefer, BaseHttpClientInvocation theInvocation) { + if (thePrefer != null) { + theInvocation.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RETURN + '=' + thePrefer.getHeaderValue()); + } + } + + private static String validateAndEscapeConditionalUrl(String theSearchUrl) { + Validate.notBlank(theSearchUrl, "Conditional URL can not be blank/null"); + StringBuilder b = new StringBuilder(); + boolean haveHadQuestionMark = false; + for (int i = 0; i < theSearchUrl.length(); i++) { + char nextChar = theSearchUrl.charAt(i); + if (!haveHadQuestionMark) { + if (nextChar == '?') { + haveHadQuestionMark = true; + } else if (!Character.isLetter(nextChar)) { + throw new IllegalArgumentException("Conditional URL must be in the format \"[ResourceType]?[Params]\" and must not have a base URL - Found: " + theSearchUrl); + } + b.append(nextChar); + } else { + switch (nextChar) { + case '|': + case '?': + case '$': + case ':': + b.append(UrlUtil.escapeUrlParam(Character.toString(nextChar))); + break; + default: + b.append(nextChar); + break; + } + } + } + return b.toString(); + } + } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseMethodBinding.java index 490ee770028..aaee9568521 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseMethodBinding.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.client.method; */ import java.io.IOException; +import java.io.InputStream; import java.io.Reader; import java.lang.reflect.Method; import java.util.*; @@ -71,11 +72,11 @@ public abstract class BaseMethodBinding implements IClientResponseHandler } - protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List> thePreferTypes) { + protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, List> thePreferTypes) { EncodingEnum encoding = EncodingEnum.forContentType(theResponseMimeType); if (encoding == null) { - NonFhirResponseException ex = NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); - populateException(ex, theResponseReader); + NonFhirResponseException ex = NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseInputStream); + populateException(ex, theResponseInputStream); throw ex; } @@ -139,7 +140,7 @@ public abstract class BaseMethodBinding implements IClientResponseHandler return mySupportsConditionalMultiple; } - protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(int theStatusCode, String theResponseMimeType, Reader theResponseReader) { + protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(int theStatusCode, String theResponseMimeType, InputStream theResponseInputStream) { BaseServerResponseException ex; switch (theStatusCode) { case Constants.STATUS_HTTP_400_BAD_REQUEST: @@ -158,9 +159,9 @@ public abstract class BaseMethodBinding implements IClientResponseHandler ex = new PreconditionFailedException("Server responded with HTTP 412"); break; case Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY: - IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theStatusCode, null); + IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseInputStream, theStatusCode, null); // TODO: handle if something other than OO comes back - BaseOperationOutcome operationOutcome = (BaseOperationOutcome) parser.parseResource(theResponseReader); + BaseOperationOutcome operationOutcome = (BaseOperationOutcome) parser.parseResource(theResponseInputStream); ex = new UnprocessableEntityException(myContext, operationOutcome); break; default: @@ -168,7 +169,7 @@ public abstract class BaseMethodBinding implements IClientResponseHandler break; } - populateException(ex, theResponseReader); + populateException(ex, theResponseInputStream); return ex; } @@ -322,9 +323,9 @@ public abstract class BaseMethodBinding implements IClientResponseHandler return theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class); } - private static void populateException(BaseServerResponseException theEx, Reader theResponseReader) { + private static void populateException(BaseServerResponseException theEx, InputStream theResponseInputStream) { try { - String responseText = IOUtils.toString(theResponseReader); + String responseText = IOUtils.toString(theResponseInputStream); theEx.setResponseBody(responseText); } catch (IOException e) { ourLog.debug("Failed to read response", e); diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseOutcomeReturningMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseOutcomeReturningMethodBinding.java index 19614ef676b..332ba6f7ddb 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseOutcomeReturningMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseOutcomeReturningMethodBinding.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.client.method; * #L% */ +import java.io.InputStream; import java.io.Reader; import java.lang.reflect.Method; import java.util.*; @@ -68,15 +69,15 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding> theHeaders) throws BaseServerResponseException { + public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { if (theResponseStatusCode >= 200 && theResponseStatusCode < 300) { if (myReturnVoid) { return null; } - MethodOutcome retVal = MethodUtil.process2xxResponse(getContext(), theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders); + MethodOutcome retVal = MethodUtil.process2xxResponse(getContext(), theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders); return retVal; } - throw processNon2xxResponseAndReturnExceptionToThrow(theResponseStatusCode, theResponseMimeType, theResponseReader); + throw processNon2xxResponseAndReturnExceptionToThrow(theResponseStatusCode, theResponseMimeType, theResponseInputStream); } public boolean isReturnVoid() { diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseResourceReturningMethodBinding.java index 0286a09b5e6..b4d2280c4fb 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseResourceReturningMethodBinding.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.rest.client.method; * #L% */ +import java.io.IOException; +import java.io.InputStream; import java.io.Reader; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -123,21 +125,21 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi public abstract ReturnTypeEnum getReturnType(); @Override - public Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) { + public Object invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws IOException { if (Constants.STATUS_HTTP_204_NO_CONTENT == theResponseStatusCode) { return toReturnType(null); } - IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theResponseStatusCode, myPreferTypesList); + IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseInputStream, theResponseStatusCode, myPreferTypesList); switch (getReturnType()) { case BUNDLE: { - IBaseBundle bundle = null; - List listOfResources = null; + IBaseBundle bundle; + List listOfResources; Class type = getContext().getResourceDefinition("Bundle").getImplementingClass(); - bundle = (IBaseBundle) parser.parseResource(type, theResponseReader); + bundle = (IBaseBundle) parser.parseResource(type, theResponseInputStream); listOfResources = BundleUtil.toListOfResources(getContext(), bundle); switch (getMethodReturnType()) { @@ -171,9 +173,9 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi case RESOURCE: { IBaseResource resource; if (myResourceType != null) { - resource = parser.parseResource(myResourceType, theResponseReader); + resource = parser.parseResource(myResourceType, theResponseInputStream); } else { - resource = parser.parseResource(theResponseReader); + resource = parser.parseResource(theResponseInputStream); } MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, resource); diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/IClientResponseHandler.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/IClientResponseHandler.java index 65437ee2d5e..dccaaac6c8a 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/IClientResponseHandler.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/IClientResponseHandler.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.client.method; */ import java.io.IOException; +import java.io.InputStream; import java.io.Reader; import java.util.List; import java.util.Map; @@ -29,6 +30,6 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; public interface IClientResponseHandler { - T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException; + T invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException; } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/IClientResponseHandlerHandlesBinary.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/IClientResponseHandlerHandlesBinary.java index ad16c57d11b..64a947b2a82 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/IClientResponseHandlerHandlesBinary.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/IClientResponseHandlerHandlesBinary.java @@ -35,6 +35,6 @@ public interface IClientResponseHandlerHandlesBinary extends IClientResponseH */ boolean isBinary(); - T invokeClient(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException; + T invokeClientForBinary(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException; } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/MethodUtil.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/MethodUtil.java index 486158974b9..94a26d60a40 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/MethodUtil.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/MethodUtil.java @@ -49,15 +49,6 @@ import ca.uhn.fhir.util.*; public class MethodUtil { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MethodUtil.class); - private static final Set ourServletRequestTypes = new HashSet(); - private static final Set ourServletResponseTypes = new HashSet(); - - static { - ourServletRequestTypes.add("javax.servlet.ServletRequest"); - ourServletResponseTypes.add("javax.servlet.ServletResponse"); - ourServletRequestTypes.add("javax.servlet.http.HttpServletRequest"); - ourServletResponseTypes.add("javax.servlet.http.HttpServletResponse"); - } /** Non instantiable */ private MethodUtil() { @@ -497,8 +488,8 @@ public class MethodUtil { } public static MethodOutcome process2xxResponse(FhirContext theContext, int theResponseStatusCode, - String theResponseMimeType, Reader theResponseReader, Map> theHeaders) { - List locationHeaders = new ArrayList(); + String theResponseMimeType, InputStream theResponseReader, Map> theHeaders) { + List locationHeaders = new ArrayList<>(); List lh = theHeaders.get(Constants.HEADER_LOCATION_LC); if (lh != null) { locationHeaders.addAll(lh); @@ -509,14 +500,14 @@ public class MethodUtil { } MethodOutcome retVal = new MethodOutcome(); - if (locationHeaders != null && locationHeaders.size() > 0) { + if (locationHeaders.size() > 0) { String locationHeader = locationHeaders.get(0); BaseOutcomeReturningMethodBinding.parseContentLocation(theContext, retVal, locationHeader); } if (theResponseStatusCode != Constants.STATUS_HTTP_204_NO_CONTENT) { EncodingEnum ct = EncodingEnum.forContentType(theResponseMimeType); if (ct != null) { - PushbackReader reader = new PushbackReader(theResponseReader); + PushbackInputStream reader = new PushbackInputStream(theResponseReader); try { int firstByte = reader.read(); diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/ReadMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/ReadMethodBinding.java index 536663c0294..24a7e163a7e 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/ReadMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/ReadMethodBinding.java @@ -106,7 +106,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem } @Override - public Object invokeClient(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map> theHeaders) + public Object invokeClientForBinary(String theResponseMimeType, InputStream theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { byte[] contents = IOUtils.toByteArray(theResponseReader); diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 75475bbddf6..c2795fa0544 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -16,14 +16,14 @@ ca.uhn.hapi.fhir hapi-fhir-base - 3.6.0 + 3.7.0-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-server - 3.6.0 + 3.7.0-SNAPSHOT true @@ -35,43 +35,43 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu2 - 3.6.0 + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-structures-hl7org-dstu2 - 3.6.0 + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-structures-dstu2.1 - 3.6.0 + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-structures-dstu3 - 3.6.0 + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-structures-r4 - 3.6.0 + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu2 - 3.6.0 + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu3 - 3.6.0 + 3.7.0-SNAPSHOT true diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index d3f54365643..29d4c168a93 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-igpacks/pom.xml b/hapi-fhir-igpacks/pom.xml index 861ba601ded..c234ef248ec 100644 --- a/hapi-fhir-igpacks/pom.xml +++ b/hapi-fhir-igpacks/pom.xml @@ -5,7 +5,7 @@ hapi-deployable-pom ca.uhn.hapi.fhir - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 7f4befcb6da..fe5524cb1f5 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index 5df5cad341f..4473c315915 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpRequest.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpRequest.java index c2cb7eeed86..654a4c52d85 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpRequest.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpRequest.java @@ -85,6 +85,11 @@ public class JaxRsHttpRequest implements IHttpRequest { return myRequestType.name(); } + @Override + public void removeHeaders(String theHeaderName) { + myHeaders.remove(theHeaderName); + } + /** * Get the Request * diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpResponse.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpResponse.java index 1212d6e7481..ea9069cf23b 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpResponse.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/client/JaxRsHttpResponse.java @@ -1,6 +1,6 @@ package ca.uhn.fhir.jaxrs.client; -import java.io.IOException; +import java.io.*; /* * #%L @@ -22,9 +22,6 @@ import java.io.IOException; * #L% */ -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -34,9 +31,11 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import ca.uhn.fhir.rest.client.impl.BaseHttpResponse; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.rest.client.api.IHttpResponse; +import org.apache.commons.io.IOUtils; /** * A Http Response based on JaxRs. This is an adapter around the class {@link javax.ws.rs.core.Response Response} @@ -118,7 +117,11 @@ public class JaxRsHttpResponse extends BaseHttpResponse implements IHttpResponse @Override public InputStream readEntity() { - return myResponse.readEntity(java.io.InputStream.class); + if (!myBufferedEntity && !myResponse.hasEntity()) { + return new ByteArrayInputStream(new byte[0]); + } else { + return new ByteArrayInputStream(myResponse.readEntity(byte[].class)); + } } @Override diff --git a/hapi-fhir-jaxrsserver-example/pom.xml b/hapi-fhir-jaxrsserver-example/pom.xml index a3a7f0ee5f8..62b224a986d 100644 --- a/hapi-fhir-jaxrsserver-example/pom.xml +++ b/hapi-fhir-jaxrsserver-example/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index f14480d8925..70d625d3b3b 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -163,7 +163,7 @@ org.thymeleaf - thymeleaf-spring4 + thymeleaf-spring5 @@ -670,7 +670,7 @@ maven-surefire-plugin alphabetical - @{argLine} -Dfile.encoding=UTF-8 -Xmx1024m + @{argLine} -Dfile.encoding=UTF-8 -Xmx20484M -Xss128M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=2048M -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC 0.6C diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 9667e2ba924..2a707982a22 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -1,5 +1,35 @@ package ca.uhn.fhir.jpa.config; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.HapiLocalizer; +import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; +import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; +import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; +import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; +import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl; +import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor; +import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor; +import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.*; +import org.springframework.core.env.Environment; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaDialect; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; +import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; + +import javax.annotation.Nonnull; + /* * #%L * HAPI FHIR JPA Server @@ -20,46 +50,14 @@ package ca.uhn.fhir.jpa.config; * #L% */ -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.i18n.HapiLocalizer; -import ca.uhn.fhir.jpa.dao.DaoRegistry; -import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; -import ca.uhn.fhir.jpa.search.*; -import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; -import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl; -import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl; -import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc; -import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; -import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl; -import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor; -import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor; -import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor; -import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.beans.factory.annotation.Autowire; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.core.env.Environment; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.orm.hibernate5.HibernateExceptionTranslator; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.HibernateJpaDialect; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.SchedulingConfigurer; -import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; -import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean; -import org.springframework.scheduling.config.ScheduledTaskRegistrar; - -import javax.annotation.Nonnull; -import java.util.concurrent.ScheduledExecutorService; - @Configuration @EnableScheduling @EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data") +@ComponentScan(basePackages = "ca.uhn.fhir.jpa", excludeFilters={ + @ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value=BaseConfig.class), + @ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value=WebSocketConfigurer.class)}) + public abstract class BaseConfig implements SchedulingConfigurer { public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor"; @@ -67,11 +65,6 @@ public abstract class BaseConfig implements SchedulingConfigurer { @Autowired protected Environment myEnv; - @Bean(name = "myDaoRegistry") - public DaoRegistry daoRegistry() { - return new DaoRegistry(); - } - @Override public void configureTasks(@Nonnull ScheduledTaskRegistrar theTaskRegistrar) { theTaskRegistrar.setTaskScheduler(taskScheduler()); @@ -95,27 +88,12 @@ public abstract class BaseConfig implements SchedulingConfigurer { public abstract FhirContext fhirContext(); - @Bean - public ICacheWarmingSvc cacheWarmingSvc() { - return new CacheWarmingSvcImpl(); - } - - @Bean - public HibernateExceptionTranslator hibernateExceptionTranslator() { - return new HibernateExceptionTranslator(); - } - - @Bean - public HibernateJpaDialect hibernateJpaDialectInstance() { - return new HibernateJpaDialect(); - } - @Bean() - public ScheduledExecutorService scheduledExecutorService() { + public ScheduledExecutorFactoryBean scheduledExecutorService() { ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean(); b.setPoolSize(5); b.afterPropertiesSet(); - return b.getObject(); + return b; } @Bean(name = "mySubscriptionTriggeringProvider") @@ -124,17 +102,28 @@ public abstract class BaseConfig implements SchedulingConfigurer { return new SubscriptionTriggeringProvider(); } - @Bean(autowire = Autowire.BY_TYPE, name = "mySearchCoordinatorSvc") - public ISearchCoordinatorSvc searchCoordinatorSvc() { - return new SearchCoordinatorSvcImpl(); + @Bean() + public TaskScheduler taskScheduler() { + ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler(); + retVal.setConcurrentExecutor(scheduledExecutorService().getObject()); + retVal.setScheduledExecutor(scheduledExecutorService().getObject()); + return retVal; + } + + @Bean(name = TASK_EXECUTOR_NAME) + public AsyncTaskExecutor taskExecutor() { + ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler(); + retVal.setConcurrentExecutor(scheduledExecutorService().getObject()); + retVal.setScheduledExecutor(scheduledExecutorService().getObject()); + return retVal; } @Bean - public ISearchParamPresenceSvc searchParamPresenceSvc() { - return new SearchParamPresenceSvcImpl(); + public IResourceReindexingSvc resourceReindexingSvc() { + return new ResourceReindexingSvcImpl(); } - @Bean(autowire = Autowire.BY_TYPE) + @Bean public IStaleSearchDeletingSvc staleSearchDeletingSvc() { return new StaleSearchDeletingSvcImpl(); } @@ -162,19 +151,6 @@ public abstract class BaseConfig implements SchedulingConfigurer { return new SubscriptionWebsocketInterceptor(); } - @Bean(name = TASK_EXECUTOR_NAME) - public TaskScheduler taskScheduler() { - ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler(); - retVal.setConcurrentExecutor(scheduledExecutorService()); - retVal.setScheduledExecutor(scheduledExecutorService()); - return retVal; - } - - @Bean - public IResourceReindexingSvc resourceReindexingSvc() { - return new ResourceReindexingSvcImpl(); - } - public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) { theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer())); theFactory.setPackagesToScan("ca.uhn.fhir.jpa.entity"); @@ -184,13 +160,4 @@ public abstract class BaseConfig implements SchedulingConfigurer { private static HibernateJpaDialect hibernateJpaDialect(HapiLocalizer theLocalizer) { return new HapiFhirHibernateJpaDialect(theLocalizer); } - - /** - * This lets the "@Value" fields reference properties from the properties file - */ - @Bean - public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { - return new PropertySourcesPlaceholderConfigurer(); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index da6135a5456..f700f4179bd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -1,126 +1,11 @@ package ca.uhn.fhir.jpa.dao; -import static org.apache.commons.lang3.StringUtils.defaultIfBlank; -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; -import static org.apache.commons.lang3.StringUtils.trim; - -import java.io.CharArrayWriter; -import java.text.Normalizer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.annotation.PostConstruct; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.PersistenceContext; -import javax.persistence.PersistenceContextType; -import javax.persistence.Tuple; -import javax.persistence.TypedQuery; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; -import javax.xml.stream.events.Characters; -import javax.xml.stream.events.XMLEvent; - +import ca.uhn.fhir.context.*; import ca.uhn.fhir.jpa.dao.data.*; -import org.apache.commons.lang3.NotImplementedException; -import org.apache.commons.lang3.Validate; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; -import org.hibernate.Session; -import org.hibernate.internal.SessionImpl; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseCoding; -import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; -import org.hl7.fhir.instance.model.api.IBaseReference; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IDomainResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.BaseResource; -import org.hl7.fhir.r4.model.Bundle.HTTPVerb; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.support.TransactionTemplate; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Charsets; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Sets; -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - -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.FhirVersionEnum; -import ca.uhn.fhir.context.RuntimeChildResourceDefinition; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.dao.index.IndexingSupport; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams; -import ca.uhn.fhir.jpa.entity.BaseHasResource; -import ca.uhn.fhir.jpa.entity.BaseTag; -import ca.uhn.fhir.jpa.entity.ForcedId; -import ca.uhn.fhir.jpa.entity.IBaseResourceEntity; -import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum; -import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; -import ca.uhn.fhir.jpa.entity.ResourceHistoryTag; -import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamCoords; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamNumber; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamQuantity; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri; -import ca.uhn.fhir.jpa.entity.ResourceLink; -import ca.uhn.fhir.jpa.entity.ResourceSearchView; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.entity.ResourceTag; -import ca.uhn.fhir.jpa.entity.Search; -import ca.uhn.fhir.jpa.entity.SearchInclude; -import ca.uhn.fhir.jpa.entity.SearchParamPresent; -import ca.uhn.fhir.jpa.entity.SearchResult; -import ca.uhn.fhir.jpa.entity.SearchStatusEnum; -import ca.uhn.fhir.jpa.entity.SearchTypeEnum; -import ca.uhn.fhir.jpa.entity.SubscriptionTable; -import ca.uhn.fhir.jpa.entity.TagDefinition; -import ca.uhn.fhir.jpa.entity.TagTypeEnum; -import ca.uhn.fhir.jpa.entity.TermCodeSystem; -import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; -import ca.uhn.fhir.jpa.entity.TermConcept; -import ca.uhn.fhir.jpa.entity.TermConceptDesignation; -import ca.uhn.fhir.jpa.entity.TermConceptMap; -import ca.uhn.fhir.jpa.entity.TermConceptMapGroup; -import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; -import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; -import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; -import ca.uhn.fhir.jpa.entity.TermConceptProperty; +import ca.uhn.fhir.jpa.dao.index.SearchParamExtractorService; +import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; @@ -129,8 +14,6 @@ import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.jpa.util.ExpungeOptions; import ca.uhn.fhir.jpa.util.ExpungeOutcome; import ca.uhn.fhir.jpa.util.JpaConstants; -import ca.uhn.fhir.model.api.IQueryParameterAnd; -import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.api.Tag; @@ -145,25 +28,11 @@ import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.LenientErrorHandler; import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.param.DateRangeParam; -import ca.uhn.fhir.rest.param.ParameterUtil; -import ca.uhn.fhir.rest.param.StringAndListParam; -import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.rest.param.TokenAndListParam; -import ca.uhn.fhir.rest.param.TokenParam; -import ca.uhn.fhir.rest.param.UriAndListParam; -import ca.uhn.fhir.rest.param.UriParam; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; -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 ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; @@ -172,6 +41,45 @@ import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.XmlUtil; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.Validate; +import org.hibernate.Session; +import org.hibernate.internal.SessionImpl; +import org.hl7.fhir.instance.model.api.*; +import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.persistence.*; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.xml.stream.events.Characters; +import javax.xml.stream.events.XMLEvent; +import java.io.CharArrayWriter; +import java.text.Normalizer; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.apache.commons.lang3.StringUtils.*; /* * #%L @@ -195,7 +103,7 @@ import ca.uhn.fhir.util.XmlUtil; @SuppressWarnings("WeakerAccess") @Repository -public abstract class BaseHapiFhirDao implements IDao, ApplicationContextAware, IndexingSupport { +public abstract class BaseHapiFhirDao implements IDao, ApplicationContextAware { public static final long INDEX_STATUS_INDEXED = 1L; public static final long INDEX_STATUS_INDEXING_FAILED = 2L; @@ -204,46 +112,17 @@ public abstract class BaseHapiFhirDao implements IDao, public static final String OO_SEVERITY_INFO = "information"; public static final String OO_SEVERITY_WARN = "warning"; public static final String UCUM_NS = "http://unitsofmeasure.org"; - static final Set EXCLUDE_ELEMENTS_IN_ENCODED; - /** - * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)} - */ - static final Map>> RESOURCE_META_AND_PARAMS; - /** - * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)} - */ - static final Map> RESOURCE_META_PARAMS; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); private static final Map ourRetrievalContexts = new HashMap(); private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest"; private static boolean ourValidationDisabledForUnitTest; private static boolean ourDisableIncrementOnUpdateForUnitTest = false; - static { - Map> resourceMetaParams = new HashMap>(); - Map>> resourceMetaAndParams = new HashMap>>(); - resourceMetaParams.put(BaseResource.SP_RES_ID, StringParam.class); - resourceMetaAndParams.put(BaseResource.SP_RES_ID, StringAndListParam.class); - resourceMetaParams.put(BaseResource.SP_RES_LANGUAGE, StringParam.class); - resourceMetaAndParams.put(BaseResource.SP_RES_LANGUAGE, StringAndListParam.class); - resourceMetaParams.put(Constants.PARAM_TAG, TokenParam.class); - resourceMetaAndParams.put(Constants.PARAM_TAG, TokenAndListParam.class); - resourceMetaParams.put(Constants.PARAM_PROFILE, UriParam.class); - resourceMetaAndParams.put(Constants.PARAM_PROFILE, UriAndListParam.class); - resourceMetaParams.put(Constants.PARAM_SECURITY, TokenParam.class); - resourceMetaAndParams.put(Constants.PARAM_SECURITY, TokenAndListParam.class); - RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams); - RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams); - - HashSet excludeElementsInEncoded = new HashSet(); - excludeElementsInEncoded.add("id"); - excludeElementsInEncoded.add("*.meta"); - EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded); - } - @PersistenceContext(type = PersistenceContextType.TRANSACTION) protected EntityManager myEntityManager; @Autowired + protected IdHelperService myIdHelperService; + @Autowired protected IForcedIdDao myForcedIdDao; @Autowired protected ISearchResultDao mySearchResultDao; @@ -255,6 +134,8 @@ public abstract class BaseHapiFhirDao implements IDao, protected IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao; @Autowired() protected IResourceIndexedSearchParamTokenDao myResourceIndexedSearchParamTokenDao; + @Autowired + protected IResourceLinkDao myResourceLinkDao; @Autowired() protected IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao; @Autowired() @@ -279,6 +160,8 @@ public abstract class BaseHapiFhirDao implements IDao, protected IResourceTagDao myResourceTagDao; @Autowired protected IResourceSearchViewDao myResourceViewDao; + @Autowired + protected ISearchParamRegistry mySearchParamRegistry; @Autowired(required = true) private DaoConfig myConfig; private FhirContext myContext; @@ -290,20 +173,20 @@ public abstract class BaseHapiFhirDao implements IDao, private ISearchParamExtractor mySearchParamExtractor; @Autowired private ISearchParamPresenceSvc mySearchParamPresenceSvc; - @Autowired - private ISearchParamRegistry mySearchParamRegistry; //@Autowired //private ISearchResultDao mySearchResultDao; @Autowired private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; - private ApplicationContext myApplicationContext; - private Map, IFhirResourceDao> myResourceTypeToDao; + @Autowired + private BeanFactory beanFactory; + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired + private MatchUrlService myMatchUrlService; + @Autowired + private SearchParamExtractorService mySearchParamExtractorService; - public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { - if (theRequestDetails != null) { - theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST); - } - } + private ApplicationContext myApplicationContext; /** * Returns the newly created forced ID. If the entity already had a forced ID, or if @@ -311,7 +194,7 @@ public abstract class BaseHapiFhirDao implements IDao, */ protected ForcedId createForcedIdIfNeeded(ResourceTable theEntity, IIdType theId, boolean theCreateForPureNumericIds) { if (theId.isEmpty() == false && theId.hasIdPart() && theEntity.getForcedId() == null) { - if (!theCreateForPureNumericIds && isValidPid(theId)) { + if (!theCreateForPureNumericIds && IdHelperService.isValidPid(theId)) { return null; } @@ -328,6 +211,7 @@ public abstract class BaseHapiFhirDao implements IDao, protected ExpungeOutcome doExpunge(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions) { TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); + txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); ourLog.info("Expunge: ResourceName[{}] Id[{}] Version[{}] Options[{}]", theResourceName, theResourceId, theVersion, theExpungeOptions); if (!getConfig().isExpungeEnabled()) { @@ -367,11 +251,14 @@ public abstract class BaseHapiFhirDao implements IDao, }); /* - * Delete any search result cache entries pointing to the given resource + * Delete any search result cache entries pointing to the given resource. We do + * this in batches to avoid sending giant batches of parameters to the DB */ - if (resourceIds.getContent().size() > 0) { + List> partitions = Lists.partition(resourceIds.getContent(), 800); + for (List nextPartition : partitions) { + ourLog.info("Expunging any search results pointing to {} resources", nextPartition.size()); txTemplate.execute(t -> { - mySearchResultDao.deleteByResourceIds(resourceIds.getContent()); + mySearchResultDao.deleteByResourceIds(nextPartition); return null; }); } @@ -438,7 +325,7 @@ public abstract class BaseHapiFhirDao implements IDao, ourLog.info("** BEGINNING GLOBAL $expunge **"); TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); - txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED); + txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); txTemplate.execute(t -> { doExpungeEverythingQuery("UPDATE " + ResourceHistoryTable.class.getSimpleName() + " d SET d.myForcedId = null"); doExpungeEverythingQuery("UPDATE " + ResourceTable.class.getSimpleName() + " d SET d.myForcedId = null"); @@ -521,6 +408,8 @@ public abstract class BaseHapiFhirDao implements IDao, myResourceIndexedSearchParamQuantityDao.deleteAll(resource.getParamsQuantity()); myResourceIndexedSearchParamStringDao.deleteAll(resource.getParamsString()); myResourceIndexedSearchParamTokenDao.deleteAll(resource.getParamsToken()); + myResourceLinkDao.deleteAll(resource.getResourceLinks()); + myResourceLinkDao.deleteAll(resource.getResourceLinksAsTarget()); myResourceTagDao.deleteAll(resource.getTags()); resource.getTags().clear(); @@ -529,7 +418,7 @@ public abstract class BaseHapiFhirDao implements IDao, ForcedId forcedId = resource.getForcedId(); resource.setForcedId(null); myResourceTableDao.saveAndFlush(resource); - myForcedIdDao.delete(forcedId); + myIdHelperService.delete(forcedId); } myResourceTableDao.delete(resource); @@ -560,7 +449,6 @@ public abstract class BaseHapiFhirDao implements IDao, } } - private void extractTagsHapi(IResource theResource, ResourceTable theEntity, Set allDefs) { TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource); if (tagList != null) { @@ -647,7 +535,7 @@ public abstract class BaseHapiFhirDao implements IDao, if (theResourceName != null) { Predicate typePredicate = builder.equal(from.get("myResourceType"), theResourceName); if (theResourceId != null) { - cq.where(typePredicate, builder.equal(from.get("myResourceId"), translateForcedIdToPid(theResourceName, theResourceId.getIdPart()))); + cq.where(typePredicate, builder.equal(from.get("myResourceId"), myIdHelperService.translateForcedIdToPid(theResourceName, theResourceId.getIdPart()))); } else { cq.where(typePredicate); } @@ -659,6 +547,7 @@ public abstract class BaseHapiFhirDao implements IDao, } } } + protected void flushJpaSession() { SessionImpl session = (SessionImpl) myEntityManager.unwrap(Session.class); int insertionCount = session.getActionQueue().numberOfInsertions(); @@ -679,8 +568,7 @@ public abstract class BaseHapiFhirDao implements IDao, return retVal; } - @Override - public DaoConfig getConfig() { + protected DaoConfig getConfig() { return myConfig; } @@ -710,51 +598,13 @@ public abstract class BaseHapiFhirDao implements IDao, } } - @Override @SuppressWarnings("unchecked") public IFhirResourceDao getDao(Class theType) { - Map, IFhirResourceDao> resourceTypeToDao = getDaos(); - IFhirResourceDao dao = (IFhirResourceDao) resourceTypeToDao.get(theType); - return dao; + return myDaoRegistry.getResourceDaoIfExists(theType); } - private Map, IFhirResourceDao> getDaos() { - if (myResourceTypeToDao == null) { - Map, IFhirResourceDao> resourceTypeToDao = new HashMap<>(); - - Map daos = myApplicationContext.getBeansOfType(IFhirResourceDao.class, false, false); - - String[] beanNames = myApplicationContext.getBeanNamesForType(IFhirResourceDao.class); - - for (IFhirResourceDao next : daos.values()) { - resourceTypeToDao.put(next.getResourceType(), next); - } - - if (this instanceof IFhirResourceDao) { - IFhirResourceDao thiz = (IFhirResourceDao) this; - resourceTypeToDao.put(thiz.getResourceType(), thiz); - } - - myResourceTypeToDao = resourceTypeToDao; - } - - return Collections.unmodifiableMap(myResourceTypeToDao); - } - - @Override - public IResourceIndexedCompositeStringUniqueDao getResourceIndexedCompositeStringUniqueDao() { - return myResourceIndexedCompositeStringUniqueDao; - } - - @Override - public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) { - Map params = mySearchParamRegistry.getActiveSearchParams(theResourceDef.getName()); - return params.get(theParamName); - } - - @Override - public Collection getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) { - return mySearchParamRegistry.getActiveSearchParams(theResourceDef.getName()).values(); + protected IFhirResourceDao getDaoOrThrowException(Class theClass) { + return myDaoRegistry.getDaoOrThrowException(theClass); } protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { @@ -892,39 +742,13 @@ public abstract class BaseHapiFhirDao implements IDao, theProvider.setSearchCoordinatorSvc(mySearchCoordinatorSvc); } - @Override public boolean isLogicalReference(IIdType theId) { - Set treatReferencesAsLogical = myConfig.getTreatReferencesAsLogical(); - if (treatReferencesAsLogical != null) { - for (String nextLogicalRef : treatReferencesAsLogical) { - nextLogicalRef = trim(nextLogicalRef); - if (nextLogicalRef.charAt(nextLogicalRef.length() - 1) == '*') { - if (theId.getValue().startsWith(nextLogicalRef.substring(0, nextLogicalRef.length() - 1))) { - return true; - } - } else { - if (theId.getValue().equals(nextLogicalRef)) { - return true; - } - } - } - - } - return false; - } - - public static void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { - if (theRequestDetails != null) { - theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE); - } + return LogicalReferenceHelper.isLogicalReference(myConfig, theId); } @Override public SearchBuilder newSearchBuilder() { - SearchBuilder builder = new SearchBuilder( - getContext(), myEntityManager, myFulltextSearchSvc, this, myResourceIndexedSearchParamUriDao, - myForcedIdDao, myTerminologySvc, mySerarchParamRegistry, myResourceTagDao, myResourceViewDao); - return builder; + return beanFactory.getBean(SearchBuilder.class, this); } public void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails theRequestDetails) { @@ -944,34 +768,6 @@ public abstract class BaseHapiFhirDao implements IDao, } } - public String parseContentTextIntoWords(IBaseResource theResource) { - StringBuilder retVal = new StringBuilder(); - @SuppressWarnings("rawtypes") - List childElements = getContext().newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class); - for (@SuppressWarnings("rawtypes") - IPrimitiveType nextType : childElements) { - if (nextType instanceof StringDt || nextType.getClass().getSimpleName().equals("StringType")) { - String nextValue = nextType.getValueAsString(); - if (isNotBlank(nextValue)) { - retVal.append(nextValue.replace("\n", " ").replace("\r", " ")); - retVal.append("\n"); - } - } - } - return retVal.toString(); - } - - @Override - public void populateFullTextFields(final IBaseResource theResource, ResourceTable theEntity) { - if (theEntity.getDeleted() != null) { - theEntity.setNarrativeTextParsedIntoWords(null); - theEntity.setContentTextParsedIntoWords(null); - } else { - theEntity.setNarrativeTextParsedIntoWords(parseNarrativeTextIntoWords(theResource)); - theEntity.setContentTextParsedIntoWords(parseContentTextIntoWords(theResource)); - } - } - private void populateResourceIdFromEntity(IBaseResourceEntity theEntity, final IBaseResource theResource) { IIdType id = theEntity.getIdDt(); if (getContext().getVersion().getVersion().isRi()) { @@ -1006,7 +802,7 @@ public abstract class BaseHapiFhirDao implements IDao, if (theEntity.getDeleted() == null) { encoding = myConfig.getResourceEncoding(); - Set excludeElements = EXCLUDE_ELEMENTS_IN_ENCODED; + Set excludeElements = ResourceMetaParams.EXCLUDE_ELEMENTS_IN_ENCODED; theEntity.setFhirVersion(myContext.getVersion().getVersion()); bytes = encodeResource(theResource, encoding, excludeElements, myContext); @@ -1247,25 +1043,6 @@ public abstract class BaseHapiFhirDao implements IDao, // nothing } - @Override - public Set processMatchUrl(String theMatchUrl, Class theResourceType) { - RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theResourceType); - - SearchParameterMap paramMap = translateMatchUrl(this, myContext, theMatchUrl, resourceDef); - paramMap.setLoadSynchronous(true); - - if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) { - throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters"); - } - - IFhirResourceDao dao = getDao(theResourceType); - if (dao == null) { - throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName()); - } - - return dao.searchForIds(paramMap); - } - @CoverageIgnore public BaseHasResource readEntity(IIdType theValueId) { throw new NotImplementedException(""); @@ -1283,7 +1060,6 @@ public abstract class BaseHapiFhirDao implements IDao, } } - /** * This method is called when an update to an existing resource detects that the resource supplied for update is missing a tag/profile/security label that the currently persisted resource holds. *

@@ -1334,11 +1110,6 @@ public abstract class BaseHapiFhirDao implements IDao, return false; } - @PostConstruct - public void startClearCaches() { - myResourceTypeToDao = null; - } - private ExpungeOutcome toExpungeOutcome(ExpungeOptions theExpungeOptions, AtomicInteger theRemainingCount) { return new ExpungeOutcome() .setDeletedCount(theExpungeOptions.getLimit() - theRemainingCount.get()); @@ -1452,7 +1223,6 @@ public abstract class BaseHapiFhirDao implements IDao, return retVal; } - @Override public String toResourceName(Class theResourceType) { return myContext.getResourceDefinition(theResourceType).getName(); } @@ -1466,16 +1236,6 @@ public abstract class BaseHapiFhirDao implements IDao, return new SliceImpl<>(Collections.singletonList(theVersion.getId())); } - @Override - public Long translateForcedIdToPid(String theResourceName, String theResourceId) { - return translateForcedIdToPids(getConfig(), new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0); - } - - protected List translateForcedIdToPids(IIdType theId) { - return translateForcedIdToPids(getConfig(), theId, myForcedIdDao); - } - - @SuppressWarnings("unchecked") protected ResourceTable updateEntity(RequestDetails theRequest, final IBaseResource theResource, ResourceTable theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing, @@ -1507,14 +1267,14 @@ public abstract class BaseHapiFhirDao implements IDao, theEntity.setPublished(theUpdateTime); } - ResourceIndexedSearchParams existingParams = new ResourceIndexedSearchParams(this, theEntity); + ResourceIndexedSearchParams existingParams = new ResourceIndexedSearchParams(theEntity); ResourceIndexedSearchParams newParams = null; EncodedResource changed; if (theDeletedTimestampOrNull != null) { - - newParams = new ResourceIndexedSearchParams(this); + + newParams = new ResourceIndexedSearchParams(); theEntity.setDeleted(theDeletedTimestampOrNull); theEntity.setUpdated(theDeletedTimestampOrNull); @@ -1530,7 +1290,8 @@ public abstract class BaseHapiFhirDao implements IDao, if (thePerformIndexing) { - newParams = new ResourceIndexedSearchParams(this, theUpdateTime, theEntity, theResource, existingParams); + newParams = new ResourceIndexedSearchParams(); + mySearchParamExtractorService.populateFromResource(newParams, this, theUpdateTime, theEntity, theResource, existingParams); changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true); @@ -1540,10 +1301,10 @@ public abstract class BaseHapiFhirDao implements IDao, } else { theEntity.setLanguage(((IAnyResource) theResource).getLanguageElement().getValue()); } - - newParams.setParams(theEntity); + + newParams.setParamsOn(theEntity); theEntity.setIndexStatus(INDEX_STATUS_INDEXED); - populateFullTextFields(theResource, theEntity); + populateFullTextFields(myContext, theResource, theEntity); } else { changed = populateResourceIntoEntity(theRequest, theResource, theEntity, false); @@ -1616,7 +1377,6 @@ public abstract class BaseHapiFhirDao implements IDao, */ if (thePerformIndexing) { Map presentSearchParams = new HashMap<>(); - // TODO KHS null check? for (String nextKey : newParams.getPopulatedResourceLinkParameters()) { presentSearchParams.put(nextKey, Boolean.TRUE); } @@ -1635,8 +1395,7 @@ public abstract class BaseHapiFhirDao implements IDao, * Indexing */ if (thePerformIndexing) { - newParams.removeCommon(theEntity, existingParams); - + mySearchParamExtractorService.removeCommon(newParams, theEntity, existingParams); } // if thePerformIndexing if (theResource != null) { @@ -1836,6 +1595,50 @@ public abstract class BaseHapiFhirDao implements IDao, } + @Override + public ISearchParamRegistry getSearchParamRegistry() { + return mySearchParamRegistry; + } + + public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { + if (theRequestDetails != null) { + theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST); + } + } + + public static void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { + if (theRequestDetails != null) { + theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE); + } + } + + public static String parseContentTextIntoWords(FhirContext theContext, IBaseResource theResource) { + StringBuilder retVal = new StringBuilder(); + @SuppressWarnings("rawtypes") + List childElements = theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, IPrimitiveType.class); + for (@SuppressWarnings("rawtypes") + IPrimitiveType nextType : childElements) { + if (nextType instanceof StringDt || nextType.getClass().getSimpleName().equals("StringType")) { + String nextValue = nextType.getValueAsString(); + if (isNotBlank(nextValue)) { + retVal.append(nextValue.replace("\n", " ").replace("\r", " ")); + retVal.append("\n"); + } + } + } + return retVal.toString(); + } + + public static void populateFullTextFields(final FhirContext theContext, final IBaseResource theResource, ResourceTable theEntity) { + if (theEntity.getDeleted() != null) { + theEntity.setNarrativeTextParsedIntoWords(null); + theEntity.setContentTextParsedIntoWords(null); + } else { + theEntity.setNarrativeTextParsedIntoWords(parseNarrativeTextIntoWords(theResource)); + theEntity.setContentTextParsedIntoWords(parseContentTextIntoWords(theContext, theResource)); + } + } + public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) { String resourceText = null; switch (theResourceEncoding) { @@ -1875,44 +1678,6 @@ public abstract class BaseHapiFhirDao implements IDao, return bytes; } - protected static boolean isValidPid(IIdType theId) { - if (theId == null || theId.getIdPart() == null) { - return false; - } - String idPart = theId.getIdPart(); - for (int i = 0; i < idPart.length(); i++) { - char nextChar = idPart.charAt(i); - if (nextChar < '0' || nextChar > '9') { - return false; - } - } - return true; - } - - @CoverageIgnore - protected static IQueryParameterAnd newInstanceAnd(String chain) { - IQueryParameterAnd type; - Class> clazz = RESOURCE_META_AND_PARAMS.get(chain); - try { - type = clazz.newInstance(); - } catch (Exception e) { - throw new InternalErrorException("Failure creating instance of " + clazz, e); - } - return type; - } - - @CoverageIgnore - protected static IQueryParameterType newInstanceType(String chain) { - IQueryParameterType type; - Class clazz = RESOURCE_META_PARAMS.get(chain); - try { - type = clazz.newInstance(); - } catch (Exception e) { - throw new InternalErrorException("Failure creating instance of " + clazz, e); - } - return type; - } - public static String normalizeString(String theString) { CharArrayWriter outBuffer = new CharArrayWriter(theString.length()); @@ -1995,169 +1760,10 @@ public abstract class BaseHapiFhirDao implements IDao, return retVal; } - protected static Long translateForcedIdToPid(DaoConfig theDaoConfig, String theResourceName, String theResourceId, IForcedIdDao - theForcedIdDao) { - return translateForcedIdToPids(theDaoConfig, new IdDt(theResourceName, theResourceId), theForcedIdDao).get(0); - } - - static List translateForcedIdToPids(DaoConfig theDaoConfig, IIdType theId, IForcedIdDao theForcedIdDao) { - Validate.isTrue(theId.hasIdPart()); - - if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(theId)) { - return Collections.singletonList(theId.getIdPartAsLong()); - } else { - List forcedId; - if (theId.hasResourceType()) { - forcedId = theForcedIdDao.findByTypeAndForcedId(theId.getResourceType(), theId.getIdPart()); - } else { - forcedId = theForcedIdDao.findByForcedId(theId.getIdPart()); - } - - if (forcedId.isEmpty() == false) { - List retVal = new ArrayList<>(forcedId.size()); - for (ForcedId next : forcedId) { - retVal.add(next.getResourcePid()); - } - return retVal; - } else { - throw new ResourceNotFoundException(theId); - } - } - } - - public static SearchParameterMap translateMatchUrl(IDao theCallingDao, FhirContext theContext, String - theMatchUrl, RuntimeResourceDefinition resourceDef) { - SearchParameterMap paramMap = new SearchParameterMap(); - List parameters = translateMatchUrl(theMatchUrl); - - ArrayListMultimap nameToParamLists = ArrayListMultimap.create(); - for (NameValuePair next : parameters) { - if (isBlank(next.getValue())) { - continue; - } - - String paramName = next.getName(); - String qualifier = null; - for (int i = 0; i < paramName.length(); i++) { - switch (paramName.charAt(i)) { - case '.': - case ':': - qualifier = paramName.substring(i); - paramName = paramName.substring(0, i); - i = Integer.MAX_VALUE - 1; - break; - } - } - - QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue()); - nameToParamLists.put(paramName, paramList); - } - - for (String nextParamName : nameToParamLists.keySet()) { - List paramList = nameToParamLists.get(nextParamName); - if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) { - if (paramList != null && paramList.size() > 0) { - if (paramList.size() > 2) { - throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions"); - } else { - DateRangeParam p1 = new DateRangeParam(); - p1.setValuesAsQueryTokens(theContext, nextParamName, paramList); - paramMap.setLastUpdated(p1); - } - } - continue; - } - - if (Constants.PARAM_HAS.equals(nextParamName)) { - IQueryParameterAnd param = ParameterUtil.parseQueryParams(theContext, RestSearchParameterTypeEnum.HAS, nextParamName, paramList); - paramMap.add(nextParamName, param); - continue; - } - - if (Constants.PARAM_COUNT.equals(nextParamName)) { - if (paramList.size() > 0 && paramList.get(0).size() > 0) { - String intString = paramList.get(0).get(0); - try { - paramMap.setCount(Integer.parseInt(intString)); - } catch (NumberFormatException e) { - throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + intString); - } - } - continue; - } - - if (RESOURCE_META_PARAMS.containsKey(nextParamName)) { - if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) { - throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier()); - } - IQueryParameterAnd type = newInstanceAnd(nextParamName); - type.setValuesAsQueryTokens(theContext, nextParamName, (paramList)); - paramMap.add(nextParamName, type); - } else if (nextParamName.startsWith("_")) { - // ignore these since they aren't search params (e.g. _sort) - } else { - RuntimeSearchParam paramDef = theCallingDao.getSearchParamByName(resourceDef, nextParamName); - if (paramDef == null) { - throw new InvalidRequestException( - "Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName); - } - - IQueryParameterAnd param = ParameterUtil.parseQueryParams(theContext, paramDef, nextParamName, paramList); - paramMap.add(nextParamName, param); - } - } - return paramMap; - } - - public static List translateMatchUrl(String theMatchUrl) { - List parameters; - String matchUrl = theMatchUrl; - int questionMarkIndex = matchUrl.indexOf('?'); - if (questionMarkIndex != -1) { - matchUrl = matchUrl.substring(questionMarkIndex + 1); - } - matchUrl = matchUrl.replace("|", "%7C"); - matchUrl = matchUrl.replace("=>=", "=%3E%3D"); - matchUrl = matchUrl.replace("=<=", "=%3C%3D"); - matchUrl = matchUrl.replace("=>", "=%3E"); - matchUrl = matchUrl.replace("=<", "=%3C"); - if (matchUrl.contains(" ")) { - throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - URL is invalid (must not contain spaces)"); - } - - parameters = URLEncodedUtils.parse((matchUrl), Constants.CHARSET_UTF8, '&'); - return parameters; - } - public static void validateResourceType(BaseHasResource theEntity, String theResourceName) { if (!theResourceName.equals(theEntity.getResourceType())) { throw new ResourceNotFoundException( "Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType()); } } - - @Override - public ISearchParamExtractor getSearchParamExtractor() { - return mySearchParamExtractor; - } - - @Override - public ISearchParamRegistry getSearchParamRegistry() { - return mySearchParamRegistry; - } - - @Override - public EntityManager getEntityManager() { - return myEntityManager; - } - - @Override - public Map, IFhirResourceDao> getResourceTypeToDao() { - return myResourceTypeToDao; - } - - @Override - public IForcedIdDao getForcedIdDao() { - return myForcedIdDao; - } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index f06d76d588e..a6e203009cd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -25,7 +25,6 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; -import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; @@ -75,8 +74,6 @@ public abstract class BaseHapiFhirResourceDao extends B protected PlatformTransactionManager myPlatformTransactionManager; @Autowired(required = false) protected IFulltextSearchSvc mySearchDao; - @Autowired() - protected ISearchResultDao mySearchResultDao; @Autowired protected DaoConfig myDaoConfig; @Autowired @@ -86,6 +83,8 @@ public abstract class BaseHapiFhirResourceDao extends B private String mySecondaryPrimaryKeyParamName; @Autowired private ISearchParamRegistry mySearchParamRegistry; + @Autowired + private MatchUrlService myMatchUrlService; @Override public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { @@ -272,7 +271,7 @@ public abstract class BaseHapiFhirResourceDao extends B public DeleteMethodOutcome deleteByUrl(String theUrl, List deleteConflicts, RequestDetails theRequest) { StopWatch w = new StopWatch(); - Set resource = processMatchUrl(theUrl, myResourceType); + Set resource = myMatchUrlService.processMatchUrl(theUrl, myResourceType); if (resource.size() > 1) { if (myDaoConfig.isAllowMultipleDelete() == false) { throw new PreconditionFailedException(getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resource.size())); @@ -372,7 +371,7 @@ public abstract class BaseHapiFhirResourceDao extends B entity.setResourceType(toResourceName(theResource)); if (isNotBlank(theIfNoneExist)) { - Set match = processMatchUrl(theIfNoneExist, myResourceType); + Set match = myMatchUrlService.processMatchUrl(theIfNoneExist, myResourceType); if (match.size() > 1) { String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size()); throw new PreconditionFailedException(msg); @@ -793,7 +792,7 @@ public abstract class BaseHapiFhirResourceDao extends B myResourceName = def.getName(); if (mySecondaryPrimaryKeyParamName != null) { - RuntimeSearchParam sp = getSearchParamByName(def, mySecondaryPrimaryKeyParamName); + RuntimeSearchParam sp = mySearchParamRegistry.getSearchParamByName(def, mySecondaryPrimaryKeyParamName); if (sp == null) { throw new ConfigurationException("Unknown search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "]"); } @@ -849,7 +848,7 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public Set processMatchUrl(String theMatchUrl) { - return processMatchUrl(theMatchUrl, getResourceType()); + return myMatchUrlService.processMatchUrl(theMatchUrl, getResourceType()); } @Override @@ -911,7 +910,7 @@ public abstract class BaseHapiFhirResourceDao extends B public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId) { validateResourceTypeAndThrowIllegalArgumentException(theId); - Long pid = translateForcedIdToPid(getResourceName(), theId.getIdPart()); + Long pid = myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart()); BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid); if (entity == null) { @@ -951,7 +950,7 @@ public abstract class BaseHapiFhirResourceDao extends B } protected ResourceTable readEntityLatestVersion(IIdType theId) { - ResourceTable entity = myEntityManager.find(ResourceTable.class, translateForcedIdToPid(getResourceName(), theId.getIdPart())); + ResourceTable entity = myEntityManager.find(ResourceTable.class, myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart())); if (entity == null) { throw new ResourceNotFoundException(theId); } @@ -1192,7 +1191,7 @@ public abstract class BaseHapiFhirResourceDao extends B // Should not be null since the check above would have caught it RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceName); - RuntimeSearchParam paramDef = getSearchParamByName(resourceDef, qualifiedParamName.getParamName()); + RuntimeSearchParam paramDef = mySearchParamRegistry.getSearchParamByName(resourceDef, qualifiedParamName.getParamName()); for (String nextValue : theSource.get(nextParamName)) { if (isNotBlank(nextValue)) { @@ -1232,7 +1231,7 @@ public abstract class BaseHapiFhirResourceDao extends B IIdType resourceId; if (isNotBlank(theMatchUrl)) { StopWatch sw = new StopWatch(); - Set match = processMatchUrl(theMatchUrl, myResourceType); + Set match = myMatchUrlService.processMatchUrl(theMatchUrl, myResourceType); if (match.size() > 1) { String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size()); throw new PreconditionFailedException(msg); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java index 830d5abbfc1..fd9be143c36 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java @@ -1,7 +1,6 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; -import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; import ca.uhn.fhir.jpa.dao.data.ITermConceptDao; import ca.uhn.fhir.jpa.util.ExpungeOptions; import ca.uhn.fhir.jpa.util.ExpungeOutcome; @@ -51,8 +50,6 @@ public abstract class BaseHapiFhirSystemDao extends BaseHapiFhirDao extends BaseHapiFhirDao implemen protected abstract RuntimeSearchParam toRuntimeSp(SP theNextSp); + @Override + public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) { + Map params = getActiveSearchParams(theResourceDef.getName()); + return params.get(theParamName); + } + + @Override + public Collection getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) { + return getActiveSearchParams(theResourceDef.getName()).values(); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index 5bc6c0879f1..e648969279f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -156,6 +156,7 @@ public class DaoConfig { private List mySearchPreFetchThresholds = Arrays.asList(500, 2000, -1); private List myWarmCacheEntries = new ArrayList<>(); private boolean myDisableHashBasedSearches; + private boolean myEnableInMemorySubscriptionMatching = true; private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC; /** @@ -1448,6 +1449,50 @@ public class DaoConfig { myDisableHashBasedSearches = theDisableHashBasedSearches; } + /** + * If set to false (default is true) the server will not use + * in-memory subscription searching and instead use the database matcher for all subscription + * criteria matching. + *

+ * When there are subscriptions registered + * on the server, the default behaviour is to compare the changed resource to the + * subscription criteria directly in-memory without going out to the database. + * Certain types of subscription criteria, e.g. chained references of queries with + * qualifiers or prefixes, are not supported by the in-memory matcher and will fall back + * to a database matcher. + *

+ * The database matcher performs a query against the + * database by prepending ?id=XYZ to the subscription criteria where XYZ is the id of the changed entity + * + * @since 3.6.1 + */ + + public boolean isEnableInMemorySubscriptionMatching() { + return myEnableInMemorySubscriptionMatching; + } + + /** + * If set to false (default is true) the server will not use + * in-memory subscription searching and instead use the database matcher for all subscription + * criteria matching. + *

+ * When there are subscriptions registered + * on the server, the default behaviour is to compare the changed resource to the + * subscription criteria directly in-memory without going out to the database. + * Certain types of subscription criteria, e.g. chained references of queries with + * qualifiers or prefixes, are not supported by the in-memory matcher and will fall back + * to a database matcher. + *

+ * The database matcher performs a query against the + * database by prepending ?id=XYZ to the subscription criteria where XYZ is the id of the changed entity + * + * @since 3.6.1 + */ + + public void setEnableInMemorySubscriptionMatching(boolean theEnableInMemorySubscriptionMatching) { + myEnableInMemorySubscriptionMatching = theEnableInMemorySubscriptionMatching; + } + public enum IndexEnabledEnum { ENABLED, DISABLED diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoRegistry.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoRegistry.java index 613e76202a5..055aadf7951 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoRegistry.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoRegistry.java @@ -23,22 +23,27 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +@Component("myDaoRegistry") public class DaoRegistry implements ApplicationContextAware { private ApplicationContext myAppCtx; @Autowired - private FhirContext myCtx; + private FhirContext myContext; + private volatile Map> myResourceNameToResourceDao; private volatile IFhirSystemDao mySystemDao; @@ -47,8 +52,8 @@ public class DaoRegistry implements ApplicationContextAware { myAppCtx = theApplicationContext; } - public IFhirSystemDao getSystemDao() { - IFhirSystemDao retVal = mySystemDao; + public IFhirSystemDao getSystemDao() { + IFhirSystemDao retVal = mySystemDao; if (retVal == null) { retVal = myAppCtx.getBean(IFhirSystemDao.class); mySystemDao = retVal; @@ -56,10 +61,11 @@ public class DaoRegistry implements ApplicationContextAware { return retVal; } - public IFhirResourceDao getResourceDao(String theResourceName) { - IFhirResourceDao retVal = getResourceNameToResourceDao().get(theResourceName); + public IFhirResourceDao getResourceDao(String theResourceName) { + init(); + IFhirResourceDao retVal = myResourceNameToResourceDao.get(theResourceName); if (retVal == null) { - List supportedResourceTypes = getResourceNameToResourceDao() + List supportedResourceTypes = myResourceNameToResourceDao .keySet() .stream() .sorted() @@ -67,26 +73,54 @@ public class DaoRegistry implements ApplicationContextAware { throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + theResourceName + " - Can handle: " + supportedResourceTypes); } return retVal; - } - public IFhirResourceDao getResourceDao(Class theResourceType) { - String resourceName = myCtx.getResourceDefinition(theResourceType).getName(); + public IFhirResourceDao getResourceDao(Class theResourceType) { + IFhirResourceDao retVal = getResourceDaoIfExists(theResourceType); + Validate.notNull(retVal, "No DAO exists for resource type %s - Have: %s", theResourceType, myResourceNameToResourceDao); + return retVal; + } + + public IFhirResourceDao getResourceDaoIfExists(Class theResourceType) { + String resourceName = myContext.getResourceDefinition(theResourceType).getName(); return (IFhirResourceDao) getResourceDao(resourceName); } - private Map> getResourceNameToResourceDao() { - Map> retVal = myResourceNameToResourceDao; - if (retVal == null || retVal.isEmpty()) { - retVal = new HashMap<>(); - Map resourceDaos = myAppCtx.getBeansOfType(IFhirResourceDao.class); - for (IFhirResourceDao nextResourceDao : resourceDaos.values()) { - RuntimeResourceDefinition nextResourceDef = myCtx.getResourceDefinition(nextResourceDao.getResourceType()); - retVal.put(nextResourceDef.getName(), nextResourceDao); - } - myResourceNameToResourceDao = retVal; + private void init() { + if (myResourceNameToResourceDao != null && !myResourceNameToResourceDao.isEmpty()) { + return; + } + + Map resourceDaos = myAppCtx.getBeansOfType(IFhirResourceDao.class); + + initializeMaps(resourceDaos.values()); + } + + private void initializeMaps(Collection theResourceDaos) { + + myResourceNameToResourceDao = new HashMap<>(); + + for (IFhirResourceDao nextResourceDao : theResourceDaos) { + RuntimeResourceDefinition nextResourceDef = myContext.getResourceDefinition(nextResourceDao.getResourceType()); + myResourceNameToResourceDao.put(nextResourceDef.getName(), nextResourceDao); + } + } + + public IFhirResourceDao getDaoOrThrowException(Class theClass) { + IFhirResourceDao retVal = getResourceDao(theClass); + if (retVal == null) { + List supportedResourceNames = myResourceNameToResourceDao + .keySet() + .stream() + .map(t -> myContext.getResourceDefinition(t).getName()) + .sorted() + .collect(Collectors.toList()); + throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + myContext.getResourceDefinition(theClass).getName() + " - Can handle: " + supportedResourceNames); } return retVal; } + public void setResourceDaos(Collection theResourceDaos) { + initializeMaps(theResourceDaos); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java index 4c2dcf968bc..1c583a4c665 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java @@ -68,7 +68,6 @@ import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import javax.persistence.TypedQuery; @@ -82,6 +81,8 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { @Autowired private PlatformTransactionManager myTxManager; @Autowired + private MatchUrlService myMatchUrlService; + @Autowired private DaoRegistry myDaoRegistry; private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) { @@ -243,7 +244,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { requestDetails.setParameters(new HashMap()); if (qIndex != -1) { String params = url.substring(qIndex); - List parameters = translateMatchUrl(params); + List parameters = myMatchUrlService.translateMatchUrl(params); for (NameValuePair next : parameters) { paramValues.put(next.getName(), next.getValue()); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java index 7b0f0930f2f..2250da4d406 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.param.StringParam; @@ -41,7 +42,6 @@ import org.hibernate.search.jpa.FullTextQuery; import org.hibernate.search.query.dsl.BooleanJunction; import org.hibernate.search.query.dsl.QueryBuilder; import org.hl7.fhir.dstu3.model.BaseResource; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Transactional; @@ -65,11 +65,14 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { @Autowired protected IForcedIdDao myForcedIdDao; - private Boolean ourDisabled; - @Autowired private DaoConfig myDaoConfig; + @Autowired + private IdHelperService myIdHelperService; + + private Boolean ourDisabled; + /** * Constructor */ @@ -225,7 +228,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { StringParam idParm = (StringParam) idParam; idParamValue = idParm.getValue(); } - pid = BaseHapiFhirDao.translateForcedIdToPid(myDaoConfig, theResourceName, idParamValue, myForcedIdDao); + pid = myIdHelperService.translateForcedIdToPid(theResourceName, idParamValue); } Long referencingPid = pid; @@ -278,7 +281,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { if (contextParts.length != 3 || "Patient".equals(contextParts[0]) == false || "$everything".equals(contextParts[2]) == false) { throw new InvalidRequestException("Invalid context: " + theContext); } - Long pid = BaseHapiFhirDao.translateForcedIdToPid( myDaoConfig, contextParts[0], contextParts[1], myForcedIdDao); + Long pid = myIdHelperService.translateForcedIdToPid(contextParts[0], contextParts[1]); FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IDao.java index 81c67874815..29a3b295ed8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IDao.java @@ -1,17 +1,13 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.entity.BaseHasResource; import ca.uhn.fhir.jpa.entity.IBaseResourceEntity; -import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.ResourceTag; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import org.hl7.fhir.instance.model.api.IBaseResource; import java.util.Collection; -import java.util.Set; /* * #%L @@ -41,10 +37,6 @@ public interface IDao { FhirContext getContext(); - RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName); - - Collection getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef); - /** * Populate all of the runtime dependencies that a bundle provider requires in order to work */ @@ -52,12 +44,9 @@ public interface IDao { ISearchBuilder newSearchBuilder(); - void populateFullTextFields(IBaseResource theResource, ResourceTable theEntity); - - Set processMatchUrl(String theMatchUrl, Class theResourceType); - IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation); R toResource(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation); + ISearchParamRegistry getSearchParamRegistry(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchBuilder.java index 0857ca7dac7..9e033a49d47 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchBuilder.java @@ -42,7 +42,7 @@ public interface ISearchBuilder { void loadResourcesByPid(Collection theIncludePids, List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation, EntityManager theEntityManager, FhirContext theContext, IDao theDao); - Set loadIncludes(IDao theCallingDao, FhirContext theContext, EntityManager theEntityManager, Collection theMatches, Set theRevIncludes, boolean theReverseMode, + Set loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection theMatches, Set theRevIncludes, boolean theReverseMode, DateRangeParam theLastUpdated, String theSearchIdOrDescription); /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java index 72b89d670b7..c617900e8af 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java @@ -20,9 +20,11 @@ package ca.uhn.fhir.jpa.dao; * #L% */ +import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -53,4 +55,8 @@ public interface ISearchParamRegistry { * Request that the cache be refreshed at the next convenient time (in a different thread) */ void requestRefresh(); + + RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName); + + Collection getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/LogicalReferenceHelper.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/LogicalReferenceHelper.java new file mode 100644 index 00000000000..8135a61bab0 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/LogicalReferenceHelper.java @@ -0,0 +1,52 @@ +package ca.uhn.fhir.jpa.dao; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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.IIdType; + +import java.util.Set; + +import static org.apache.commons.lang3.StringUtils.trim; + +public class LogicalReferenceHelper { + + public static boolean isLogicalReference(DaoConfig myConfig, IIdType theId) { + Set treatReferencesAsLogical = myConfig.getTreatReferencesAsLogical(); + if (treatReferencesAsLogical != null) { + for (String nextLogicalRef : treatReferencesAsLogical) { + nextLogicalRef = trim(nextLogicalRef); + if (nextLogicalRef.charAt(nextLogicalRef.length() - 1) == '*') { + if (theId.getValue().startsWith(nextLogicalRef.substring(0, nextLogicalRef.length() - 1))) { + return true; + } + } else { + if (theId.getValue().equals(nextLogicalRef)) { + return true; + } + } + } + + } + return false; + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/MatchUrlService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/MatchUrlService.java new file mode 100644 index 00000000000..72d55bb383b --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/MatchUrlService.java @@ -0,0 +1,206 @@ +package ca.uhn.fhir.jpa.dao; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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 ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.model.api.IQueryParameterAnd; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.QualifiedParamList; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.ParameterUtil; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.util.CoverageIgnore; +import com.google.common.collect.ArrayListMultimap; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Set; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Service +public class MatchUrlService { + + @Autowired + private FhirContext myContext; + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired + private MatchUrlService myMatchUrlService; + @Autowired + private ISearchParamRegistry mySearchParamRegistry; + + public Set processMatchUrl(String theMatchUrl, Class theResourceType) { + RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceType); + + SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(theMatchUrl, resourceDef); + paramMap.setLoadSynchronous(true); + + if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) { + throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters"); + } + + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResourceType); + if (dao == null) { + throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName()); + } + + return dao.searchForIds(paramMap); + } + + public SearchParameterMap translateMatchUrl(String + theMatchUrl, RuntimeResourceDefinition resourceDef) { + SearchParameterMap paramMap = new SearchParameterMap(); + List parameters = translateMatchUrl(theMatchUrl); + + ArrayListMultimap nameToParamLists = ArrayListMultimap.create(); + for (NameValuePair next : parameters) { + if (isBlank(next.getValue())) { + continue; + } + + String paramName = next.getName(); + String qualifier = null; + for (int i = 0; i < paramName.length(); i++) { + switch (paramName.charAt(i)) { + case '.': + case ':': + qualifier = paramName.substring(i); + paramName = paramName.substring(0, i); + i = Integer.MAX_VALUE - 1; + break; + } + } + + QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue()); + nameToParamLists.put(paramName, paramList); + } + + for (String nextParamName : nameToParamLists.keySet()) { + List paramList = nameToParamLists.get(nextParamName); + if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) { + if (paramList != null && paramList.size() > 0) { + if (paramList.size() > 2) { + throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions"); + } else { + DateRangeParam p1 = new DateRangeParam(); + p1.setValuesAsQueryTokens(myContext, nextParamName, paramList); + paramMap.setLastUpdated(p1); + } + } + continue; + } + + if (Constants.PARAM_HAS.equals(nextParamName)) { + IQueryParameterAnd param = ParameterUtil.parseQueryParams(myContext, RestSearchParameterTypeEnum.HAS, nextParamName, paramList); + paramMap.add(nextParamName, param); + continue; + } + + if (Constants.PARAM_COUNT.equals(nextParamName)) { + if (paramList.size() > 0 && paramList.get(0).size() > 0) { + String intString = paramList.get(0).get(0); + try { + paramMap.setCount(Integer.parseInt(intString)); + } catch (NumberFormatException e) { + throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + intString); + } + } + continue; + } + + if (ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(nextParamName)) { + if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) { + throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier()); + } + IQueryParameterAnd type = newInstanceAnd(nextParamName); + type.setValuesAsQueryTokens(myContext, nextParamName, (paramList)); + paramMap.add(nextParamName, type); + } else if (nextParamName.startsWith("_")) { + // ignore these since they aren't search params (e.g. _sort) + } else { + RuntimeSearchParam paramDef = mySearchParamRegistry.getSearchParamByName(resourceDef, nextParamName); + if (paramDef == null) { + throw new InvalidRequestException( + "Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName); + } + + IQueryParameterAnd param = ParameterUtil.parseQueryParams(myContext, paramDef, nextParamName, paramList); + paramMap.add(nextParamName, param); + } + } + return paramMap; + } + + public List translateMatchUrl(String theMatchUrl) { + List parameters; + String matchUrl = theMatchUrl; + int questionMarkIndex = matchUrl.indexOf('?'); + if (questionMarkIndex != -1) { + matchUrl = matchUrl.substring(questionMarkIndex + 1); + } + matchUrl = matchUrl.replace("|", "%7C"); + matchUrl = matchUrl.replace("=>=", "=%3E%3D"); + matchUrl = matchUrl.replace("=<=", "=%3C%3D"); + matchUrl = matchUrl.replace("=>", "=%3E"); + matchUrl = matchUrl.replace("=<", "=%3C"); + if (matchUrl.contains(" ")) { + throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - URL is invalid (must not contain spaces)"); + } + + parameters = URLEncodedUtils.parse((matchUrl), Constants.CHARSET_UTF8, '&'); + return parameters; + } + + @CoverageIgnore + protected IQueryParameterAnd newInstanceAnd(String chain) { + IQueryParameterAnd type; + Class clazz = ResourceMetaParams.RESOURCE_META_AND_PARAMS.get(chain); + try { + type = clazz.newInstance(); + } catch (Exception e) { + throw new InternalErrorException("Failure creating instance of " + clazz, e); + } + return type; + } + + @CoverageIgnore + public IQueryParameterType newInstanceType(String chain) { + IQueryParameterType type; + Class clazz = ResourceMetaParams.RESOURCE_META_PARAMS.get(chain); + try { + type = clazz.newInstance(); + } catch (Exception e) { + throw new InternalErrorException("Failure creating instance of " + clazz, e); + } + return type; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ResourceMetaParams.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ResourceMetaParams.java new file mode 100644 index 00000000000..17e193cfeac --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ResourceMetaParams.java @@ -0,0 +1,63 @@ +package ca.uhn.fhir.jpa.dao; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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.model.api.IQueryParameterAnd; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.param.*; +import org.hl7.fhir.r4.model.BaseResource; + +import java.util.*; + +public class ResourceMetaParams { + /** + * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)} + */ + public static final Map>> RESOURCE_META_AND_PARAMS; + /** + * These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)} + */ + public static final Map> RESOURCE_META_PARAMS; + public static final Set EXCLUDE_ELEMENTS_IN_ENCODED; + + static { + Map> resourceMetaParams = new HashMap>(); + Map>> resourceMetaAndParams = new HashMap>>(); + resourceMetaParams.put(BaseResource.SP_RES_ID, StringParam.class); + resourceMetaAndParams.put(BaseResource.SP_RES_ID, StringAndListParam.class); + resourceMetaParams.put(BaseResource.SP_RES_LANGUAGE, StringParam.class); + resourceMetaAndParams.put(BaseResource.SP_RES_LANGUAGE, StringAndListParam.class); + resourceMetaParams.put(Constants.PARAM_TAG, TokenParam.class); + resourceMetaAndParams.put(Constants.PARAM_TAG, TokenAndListParam.class); + resourceMetaParams.put(Constants.PARAM_PROFILE, UriParam.class); + resourceMetaAndParams.put(Constants.PARAM_PROFILE, UriAndListParam.class); + resourceMetaParams.put(Constants.PARAM_SECURITY, TokenParam.class); + resourceMetaAndParams.put(Constants.PARAM_SECURITY, TokenAndListParam.class); + RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams); + RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams); + + HashSet excludeElementsInEncoded = new HashSet(); + excludeElementsInEncoded.add("id"); + excludeElementsInEncoded.add("*.meta"); + EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index 40a912fef2b..87ffe7b0811 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -21,10 +21,11 @@ package ca.uhn.fhir.jpa.dao; */ import ca.uhn.fhir.context.*; -import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao; import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams; import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; @@ -69,10 +70,15 @@ import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPre import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; import javax.persistence.TypedQuery; import javax.persistence.criteria.*; import java.math.BigDecimal; @@ -87,6 +93,8 @@ import static org.apache.commons.lang3.StringUtils.*; * searches for resources */ @SuppressWarnings("JpaQlInspection") +@Component +@Scope("prototype") public class SearchBuilder implements ISearchBuilder { private static final List EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>()); @@ -97,26 +105,42 @@ public class SearchBuilder implements ISearchBuilder { private static String ourLastHandlerThreadForUnitTest; private static boolean ourTrackHandlersForUnitTest; private final boolean myDontUseHashesForSearch; + private final DaoConfig myDaoConfig; + + @Autowired protected IResourceTagDao myResourceTagDao; + @Autowired private IResourceSearchViewDao myResourceSearchViewDao; + @Autowired + private FhirContext myContext; + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + protected EntityManager myEntityManager; + @Autowired + private IdHelperService myIdHelperService; + @Autowired(required = false) + private IFulltextSearchSvc myFulltextSearchSvc; + @Autowired + private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao; + @Autowired + private ISearchParamRegistry mySearchParamRegistry; + @Autowired + private IHapiTerminologySvc myTerminologySvc; + @Autowired + private MatchUrlService myMatchUrlService; + @Autowired + private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; + private List myAlsoIncludePids; private CriteriaBuilder myBuilder; private BaseHapiFhirDao myCallingDao; - private FhirContext myContext; - private EntityManager myEntityManager; - private IForcedIdDao myForcedIdDao; - private IFulltextSearchSvc myFulltextSearchSvc; private Map> myIndexJoins = Maps.newHashMap(); private SearchParameterMap myParams; private ArrayList myPredicates; - private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao; private String myResourceName; private AbstractQuery myResourceTableQuery; private Root myResourceTableRoot; private Class myResourceType; - private ISearchParamRegistry mySearchParamRegistry; private String mySearchUuid; - private IHapiTerminologySvc myTerminologySvc; private int myFetchSize; private Integer myMaxResultsToFetch; private Set myPidSet; @@ -124,22 +148,10 @@ public class SearchBuilder implements ISearchBuilder { /** * Constructor */ - SearchBuilder(FhirContext theFhirContext, EntityManager theEntityManager, - IFulltextSearchSvc theFulltextSearchSvc, BaseHapiFhirDao theDao, - IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao, - IHapiTerminologySvc theTerminologySvc, ISearchParamRegistry theSearchParamRegistry, - IResourceTagDao theResourceTagDao, IResourceSearchViewDao theResourceViewDao) { - myContext = theFhirContext; - myEntityManager = theEntityManager; - myFulltextSearchSvc = theFulltextSearchSvc; + SearchBuilder(BaseHapiFhirDao theDao) { myCallingDao = theDao; - myDontUseHashesForSearch = theDao.getConfig().getDisableHashBasedSearches(); - myResourceIndexedSearchParamUriDao = theResourceIndexedSearchParamUriDao; - myForcedIdDao = theForcedIdDao; - myTerminologySvc = theTerminologySvc; - mySearchParamRegistry = theSearchParamRegistry; - myResourceTagDao = theResourceTagDao; - myResourceSearchViewDao = theResourceViewDao; + myDaoConfig = theDao.getConfig(); + myDontUseHashesForSearch = myDaoConfig.getDisableHashBasedSearches(); } @Override @@ -220,24 +232,24 @@ public class SearchBuilder implements ISearchBuilder { assert parameterName != null; String paramName = parameterName.replaceAll("\\..*", ""); - RuntimeSearchParam owningParameterDef = myCallingDao.getSearchParamByName(targetResourceDefinition, paramName); + RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName); if (owningParameterDef == null) { throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + parameterName); } - owningParameterDef = myCallingDao.getSearchParamByName(targetResourceDefinition, owningParameter); + owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, owningParameter); if (owningParameterDef == null) { throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + owningParameter); } Class resourceType = targetResourceDefinition.getImplementingClass(); - Set match = myCallingDao.processMatchUrl(matchUrl, resourceType); + Set match = myMatchUrlService.processMatchUrl(matchUrl, resourceType); if (match.isEmpty()) { // Pick a PID that can never match match = Collections.singleton(-1L); } - Join join = myResourceTableRoot.join("myIncomingResourceLinks", JoinType.LEFT); + Join join = myResourceTableRoot.join("myResourceLinksAsTarget", JoinType.LEFT); Predicate predicate = join.get("mySourceResourcePid").in(match); myPredicates.add(predicate); @@ -373,7 +385,7 @@ public class SearchBuilder implements ISearchBuilder { IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null); if (dt.hasBaseUrl()) { - if (myCallingDao.getConfig().getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) { + if (myDaoConfig.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) { dt = dt.toUnqualified(); } else { ourLog.debug("Searching for resource link with target URL: {}", dt.getValue()); @@ -385,7 +397,7 @@ public class SearchBuilder implements ISearchBuilder { List targetPid; try { - targetPid = myCallingDao.translateForcedIdToPids(dt); + targetPid = myIdHelperService.translateForcedIdToPids(dt); } catch (ResourceNotFoundException e) { // Use a PID that will never exist targetPid = Collections.singletonList(-1L); @@ -417,7 +429,7 @@ public class SearchBuilder implements ISearchBuilder { if (resourceTypes.isEmpty()) { RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceName); - RuntimeSearchParam searchParamByName = myCallingDao.getSearchParamByName(resourceDef, theParamName); + RuntimeSearchParam searchParamByName = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName); if (searchParamByName == null) { throw new InternalErrorException("Could not find parameter " + theParamName); } @@ -491,10 +503,10 @@ public class SearchBuilder implements ISearchBuilder { chain = chain.substring(0, qualifierIndex); } - boolean isMeta = BaseHapiFhirDao.RESOURCE_META_PARAMS.containsKey(chain); + boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain); RuntimeSearchParam param = null; if (!isMeta) { - param = myCallingDao.getSearchParamByName(typeDef, chain); + param = mySearchParamRegistry.getSearchParamByName(typeDef, chain); if (param == null) { ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param); continue; @@ -512,7 +524,7 @@ public class SearchBuilder implements ISearchBuilder { chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); ((ReferenceParam) chainValue).setChain(remainingChain); } else if (isMeta) { - IQueryParameterType type = BaseHapiFhirDao.newInstanceType(chain); + IQueryParameterType type = myMatchUrlService.newInstanceType(chain); type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); chainValue = type; } else { @@ -1162,7 +1174,6 @@ public class SearchBuilder implements ISearchBuilder { private Predicate createPredicateString(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder, From theFrom) { String rawSearchTerm; - DaoConfig daoConfig = myCallingDao.getConfig(); if (theParameter instanceof TokenParam) { TokenParam id = (TokenParam) theParameter; if (!id.isText()) { @@ -1173,7 +1184,7 @@ public class SearchBuilder implements ISearchBuilder { StringParam id = (StringParam) theParameter; rawSearchTerm = id.getValue(); if (id.isContains()) { - if (!daoConfig.isAllowContainsSearches()) { + if (!myDaoConfig.isAllowContainsSearches()) { throw new MethodNotAllowedException(":contains modifier is disabled on this server"); } } @@ -1191,7 +1202,7 @@ public class SearchBuilder implements ISearchBuilder { if (myDontUseHashesForSearch) { String likeExpression = BaseHapiFhirDao.normalizeString(rawSearchTerm); - if (myCallingDao.getConfig().isAllowContainsSearches()) { + if (myDaoConfig.isAllowContainsSearches()) { if (theParameter instanceof StringParam) { if (((StringParam) theParameter).isContains()) { likeExpression = createLeftAndRightMatchLikeExpression(likeExpression); @@ -1230,13 +1241,13 @@ public class SearchBuilder implements ISearchBuilder { String likeExpression; if (theParameter instanceof StringParam && ((StringParam) theParameter).isContains() && - daoConfig.isAllowContainsSearches()) { + myDaoConfig.isAllowContainsSearches()) { likeExpression = createLeftAndRightMatchLikeExpression(normalizedString); } else { likeExpression = createLeftMatchLikeExpression(normalizedString); } - Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(daoConfig, theResourceName, theParamName, normalizedString); + Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(myDaoConfig, theResourceName, theParamName, normalizedString); Predicate hashCode = theBuilder.equal(theFrom.get("myHashNormalizedPrefix").as(Long.class), hash); Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); return theBuilder.and(hashCode, singleCode); @@ -1473,7 +1484,7 @@ public class SearchBuilder implements ISearchBuilder { * of parameters passed in */ ourLog.debug("Checking for unique index for query: {}", theParams.toNormalizedQueryString(myContext)); - if (myCallingDao.getConfig().isUniqueIndexesEnabled()) { + if (myDaoConfig.isUniqueIndexesEnabled()) { if (myParams.getIncludes().isEmpty()) { if (myParams.getRevIncludes().isEmpty()) { if (myParams.getEverythingMode() == null) { @@ -1600,7 +1611,7 @@ public class SearchBuilder implements ISearchBuilder { if (myParams.get(IAnyResource.SP_RES_ID) != null) { StringParam idParm = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0); - Long pid = BaseHapiFhirDao.translateForcedIdToPid(myCallingDao.getConfig(), myResourceName, idParm.getValue(), myForcedIdDao); + Long pid = myIdHelperService.translateForcedIdToPid(myResourceName, idParm.getValue()); if (myAlsoIncludePids == null) { myAlsoIncludePids = new ArrayList<>(1); } @@ -1677,7 +1688,7 @@ public class SearchBuilder implements ISearchBuilder { } private Predicate createResourceLinkPathPredicate(String theResourceName, String theParamName, From from) { - return createResourceLinkPathPredicate(myCallingDao, myContext, theParamName, from, theResourceName); + return createResourceLinkPathPredicate(myContext, theParamName, from, theResourceName); } /** @@ -1713,7 +1724,7 @@ public class SearchBuilder implements ISearchBuilder { } RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName); - RuntimeSearchParam param = myCallingDao.getSearchParamByName(resourceDef, theSort.getParamName()); + RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theSort.getParamName()); if (param == null) { throw new InvalidRequestException("Unknown sort parameter '" + theSort.getParamName() + "'"); } @@ -1807,7 +1818,7 @@ public class SearchBuilder implements ISearchBuilder { String retVal = theSystem; if (retVal == null) { RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName); - RuntimeSearchParam param = myCallingDao.getSearchParamByName(resourceDef, theParamName); + RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName); if (param != null) { Set valueSetUris = Sets.newHashSet(); for (String nextPath : param.getPathsSplit()) { @@ -1957,7 +1968,7 @@ public class SearchBuilder implements ISearchBuilder { * so it can't be Collections.emptySet() or some such thing */ @Override - public HashSet loadIncludes(IDao theCallingDao, FhirContext theContext, EntityManager theEntityManager, Collection theMatches, Set theRevIncludes, + public HashSet loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection theMatches, Set theRevIncludes, boolean theReverseMode, DateRangeParam theLastUpdated, String theSearchIdOrDescription) { if (theMatches.size() == 0) { return new HashSet<>(); @@ -2017,7 +2028,7 @@ public class SearchBuilder implements ISearchBuilder { String paramName = nextInclude.getParamName(); if (isNotBlank(paramName)) { - param = theCallingDao.getSearchParamByName(def, paramName); + param = mySearchParamRegistry.getSearchParamByName(def, paramName); } else { param = null; } @@ -2088,6 +2099,7 @@ public class SearchBuilder implements ISearchBuilder { private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams) { myParams = theParams; + theParams.clean(); for (Entry>> nextParamEntry : myParams.entrySet()) { String nextParamName = nextParamEntry.getKey(); List> andOrParams = nextParamEntry.getValue(); @@ -2099,37 +2111,6 @@ public class SearchBuilder implements ISearchBuilder { private void searchForIdsWithAndOr(String theResourceName, String theParamName, List> theAndOrParams) { - /* - * Filter out - */ - for (int andListIdx = 0; andListIdx < theAndOrParams.size(); andListIdx++) { - List nextOrList = theAndOrParams.get(andListIdx); - - for (int orListIdx = 0; orListIdx < nextOrList.size(); orListIdx++) { - IQueryParameterType nextOr = nextOrList.get(orListIdx); - boolean hasNoValue = false; - if (nextOr.getMissing() != null) { - continue; - } - if (nextOr instanceof QuantityParam) { - if (isBlank(((QuantityParam) nextOr).getValueAsString())) { - hasNoValue = true; - } - } - - if (hasNoValue) { - ourLog.debug("Ignoring empty parameter: {}", theParamName); - nextOrList.remove(orListIdx); - orListIdx--; - } - } - - if (nextOrList.isEmpty()) { - theAndOrParams.remove(andListIdx); - andListIdx--; - } - } - if (theAndOrParams.isEmpty()) { return; } @@ -2288,7 +2269,7 @@ public class SearchBuilder implements ISearchBuilder { private int myCurrentOffset; private ArrayList myCurrentPids; private Long myNext; - private int myPageSize = myCallingDao.getConfig().getEverythingIncludesFetchPageSize(); + private int myPageSize = myDaoConfig.getEverythingIncludesFetchPageSize(); IncludesIterator(Set thePidSet) { myCurrentPids = new ArrayList<>(thePidSet); @@ -2316,7 +2297,7 @@ public class SearchBuilder implements ISearchBuilder { myCurrentOffset = end; Collection pidsToScan = myCurrentPids.subList(start, end); Set includes = Collections.singleton(new Include("*", true)); - Set newPids = loadIncludes(myCallingDao, myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated(), mySearchUuid); + Set newPids = loadIncludes(myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated(), mySearchUuid); myCurrentIterator = newPids.iterator(); } @@ -2368,7 +2349,7 @@ public class SearchBuilder implements ISearchBuilder { // If we don't have a query yet, create one if (myResultsIterator == null) { if (myMaxResultsToFetch == null) { - myMaxResultsToFetch = myCallingDao.getConfig().getFetchSizeDefaultMaximum(); + myMaxResultsToFetch = myDaoConfig.getFetchSizeDefaultMaximum(); } final TypedQuery query = createQuery(mySort, myMaxResultsToFetch, false); @@ -2483,7 +2464,7 @@ public class SearchBuilder implements ISearchBuilder { if (myWrap == null) { ourLog.debug("Searching for unique index matches over {} candidate query strings", myUniqueQueryStrings.size()); StopWatch sw = new StopWatch(); - Collection resourcePids = myCallingDao.getResourceIndexedCompositeStringUniqueDao().findResourcePidsByQueryStrings(myUniqueQueryStrings); + Collection resourcePids = myResourceIndexedCompositeStringUniqueDao.findResourcePidsByQueryStrings(myUniqueQueryStrings); ourLog.debug("Found {} unique index matches in {}ms", resourcePids.size(), sw.getMillis()); myWrap = resourcePids.iterator(); } @@ -2621,10 +2602,10 @@ public class SearchBuilder implements ISearchBuilder { return likeExpression.replace("%", "[%]") + "%"; } - private static Predicate createResourceLinkPathPredicate(IDao theCallingDao, FhirContext theContext, String theParamName, From theFrom, + private Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, From theFrom, String theResourceType) { RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType); - RuntimeSearchParam param = theCallingDao.getSearchParamByName(resourceDef, theParamName); + RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName); List path = param.getPathsSplit(); /* diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java index 111656582d0..db5ab34b071 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.api.*; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.QuantityParam; import ca.uhn.fhir.util.ObjectUtil; import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.lang3.StringUtils; @@ -17,6 +18,7 @@ import org.apache.commons.lang3.builder.ToStringStyle; import java.util.*; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; /* @@ -40,6 +42,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public class SearchParameterMap extends LinkedHashMap>> { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMap.class); private static final long serialVersionUID = 1L; @@ -295,7 +298,7 @@ public class SearchParameterMap extends LinkedHashMap - * ?name=smith&_sort=Patient:family + * ?name=smith&_sort=Patient:family *

*

* This method excludes the _count parameter, @@ -336,6 +339,10 @@ public class SearchParameterMap extends LinkedHashMap { @Autowired private ITransactionProcessorVersionAdapter myVersionAdapter; @Autowired + private MatchUrlService myMatchUrlService; + @Autowired private DaoRegistry myDaoRegistry; private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) { @@ -390,7 +395,7 @@ public class TransactionProcessor { requestDetails.setParameters(new HashMap<>()); if (qIndex != -1) { String params = url.substring(qIndex); - List parameters = BaseHapiFhirDao.translateMatchUrl(params); + List parameters = myMatchUrlService.translateMatchUrl(params); for (NameValuePair next : parameters) { paramValues.put(next.getName(), next.getValue()); } @@ -481,284 +486,297 @@ public class TransactionProcessor { return myContext.getVersion().newIdType().setValue(theValue); } - private Map doTransactionWriteOperations(ServletRequestDetails theRequestDetails, String theActionName, Date theUpdateTime, Set theAllIds, + + private Map doTransactionWriteOperations(final ServletRequestDetails theRequestDetails, String theActionName, Date theUpdateTime, Set theAllIds, Map theIdSubstitutions, Map theIdToPersistedOutcome, BUNDLE theResponse, IdentityHashMap theOriginalRequestOrder, List theEntries, StopWatch theTransactionStopWatch) { - Set deletedResources = new HashSet<>(); - List deleteConflicts = new ArrayList<>(); - Map entriesToProcess = new IdentityHashMap<>(); - Set nonUpdatedEntities = new HashSet<>(); - Set updatedEntities = new HashSet<>(); - Map> conditionalRequestUrls = new HashMap<>(); - /* - * Loop through the request and process any entries of type - * PUT, POST or DELETE - */ - for (int i = 0; i < theEntries.size(); i++) { + if (theRequestDetails != null) { + theRequestDetails.startDeferredOperationCallback(); + } + try { - if (i % 100 == 0) { - ourLog.debug("Processed {} non-GET entries out of {}", i, theEntries.size()); - } + Set deletedResources = new HashSet<>(); + List deleteConflicts = new ArrayList<>(); + Map entriesToProcess = new IdentityHashMap<>(); + Set nonUpdatedEntities = new HashSet<>(); + Set updatedEntities = new HashSet<>(); + Map> conditionalRequestUrls = new HashMap<>(); - BUNDLEENTRY nextReqEntry = theEntries.get(i); - IBaseResource res = myVersionAdapter.getResource(nextReqEntry); - IIdType nextResourceId = null; - if (res != null) { + /* + * Loop through the request and process any entries of type + * PUT, POST or DELETE + */ + for (int i = 0; i < theEntries.size(); i++) { - nextResourceId = res.getIdElement(); - - if (!nextResourceId.hasIdPart()) { - if (isNotBlank(myVersionAdapter.getFullUrl(nextReqEntry))) { - nextResourceId = newIdType(myVersionAdapter.getFullUrl(nextReqEntry)); - } + if (i % 100 == 0) { + ourLog.debug("Processed {} non-GET entries out of {}", i, theEntries.size()); } - if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) { - throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); - } + BUNDLEENTRY nextReqEntry = theEntries.get(i); + IBaseResource res = myVersionAdapter.getResource(nextReqEntry); + IIdType nextResourceId = null; + if (res != null) { - if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) { - nextResourceId = newIdType(toResourceName(res.getClass()), nextResourceId.getIdPart()); - res.setId(nextResourceId); - } + nextResourceId = res.getIdElement(); - /* - * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness - */ - if (isPlaceholder(nextResourceId)) { - if (!theAllIds.add(nextResourceId)) { - throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId)); - } - } else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) { - IIdType nextId = nextResourceId.toUnqualifiedVersionless(); - if (!theAllIds.add(nextId)) { - throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId)); - } - } - - } - - String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry); - String resourceType = res != null ? myContext.getResourceDefinition(res).getName() : null; - Integer order = theOriginalRequestOrder.get(nextReqEntry); - BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(theResponse).get(order); - - theTransactionStopWatch.startTask("Bundle.entry[" + i + "]: " + verb + " " + defaultString(resourceType)); - - switch (verb) { - case "POST": { - // CREATE - @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); - res.setId((String) null); - DaoMethodOutcome outcome; - String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry); - matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - outcome = resourceDao.create(res, matchUrl, false, theRequestDetails); - if (nextResourceId != null) { - handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); - } - entriesToProcess.put(nextRespEntry, outcome.getEntity()); - if (outcome.getCreated() == false) { - nonUpdatedEntities.add(outcome.getEntity()); - } else { - if (isNotBlank(matchUrl)) { - conditionalRequestUrls.put(matchUrl, res.getClass()); + if (!nextResourceId.hasIdPart()) { + if (isNotBlank(myVersionAdapter.getFullUrl(nextReqEntry))) { + nextResourceId = newIdType(myVersionAdapter.getFullUrl(nextReqEntry)); + } + } + + if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) { + throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); + } + + if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) { + nextResourceId = newIdType(toResourceName(res.getClass()), nextResourceId.getIdPart()); + res.setId(nextResourceId); + } + + /* + * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness + */ + if (isPlaceholder(nextResourceId)) { + if (!theAllIds.add(nextResourceId)) { + throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId)); + } + } else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) { + IIdType nextId = nextResourceId.toUnqualifiedVersionless(); + if (!theAllIds.add(nextId)) { + throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId)); } } - break; } - case "DELETE": { - // DELETE - String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); - UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); - ca.uhn.fhir.jpa.dao.IFhirResourceDao dao = toDao(parts, verb, url); - int status = Constants.STATUS_HTTP_204_NO_CONTENT; - if (parts.getResourceId() != null) { - IIdType deleteId = newIdType(parts.getResourceType(), parts.getResourceId()); - if (!deletedResources.contains(deleteId.getValueAsString())) { - DaoMethodOutcome outcome = dao.delete(deleteId, deleteConflicts, theRequestDetails); - if (outcome.getEntity() != null) { - deletedResources.add(deleteId.getValueAsString()); - entriesToProcess.put(nextRespEntry, outcome.getEntity()); + + String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry); + String resourceType = res != null ? myContext.getResourceDefinition(res).getName() : null; + Integer order = theOriginalRequestOrder.get(nextReqEntry); + BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(theResponse).get(order); + + theTransactionStopWatch.startTask("Bundle.entry[" + i + "]: " + verb + " " + defaultString(resourceType)); + + switch (verb) { + case "POST": { + // CREATE + @SuppressWarnings("rawtypes") + IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); + res.setId((String) null); + DaoMethodOutcome outcome; + String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry); + matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); + outcome = resourceDao.create(res, matchUrl, false, theRequestDetails); + if (nextResourceId != null) { + handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); + } + entriesToProcess.put(nextRespEntry, outcome.getEntity()); + if (outcome.getCreated() == false) { + nonUpdatedEntities.add(outcome.getEntity()); + } else { + if (isNotBlank(matchUrl)) { + conditionalRequestUrls.put(matchUrl, res.getClass()); } } - } else { - String matchUrl = parts.getResourceType() + '?' + parts.getParams(); - matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequestDetails); - List allDeleted = deleteOutcome.getDeletedEntities(); - for (ResourceTable deleted : allDeleted) { - deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString()); - } - if (allDeleted.isEmpty()) { - status = Constants.STATUS_HTTP_204_NO_CONTENT; - } - myVersionAdapter.setResponseOutcome(nextRespEntry, deleteOutcome.getOperationOutcome()); + break; } - - myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(status)); - - break; - } - case "PUT": { - // UPDATE - @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); - - String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); - - DaoMethodOutcome outcome; - UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); - if (isNotBlank(parts.getResourceId())) { - String version = null; - if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) { - version = ParameterUtil.parseETagValue(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry)); - } - res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version)); - outcome = resourceDao.update(res, null, false, theRequestDetails); - } else { - res.setId((String) null); - String matchUrl; - if (isNotBlank(parts.getParams())) { - matchUrl = parts.getResourceType() + '?' + parts.getParams(); + case "DELETE": { + // DELETE + String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); + UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); + ca.uhn.fhir.jpa.dao.IFhirResourceDao dao = toDao(parts, verb, url); + int status = Constants.STATUS_HTTP_204_NO_CONTENT; + if (parts.getResourceId() != null) { + IIdType deleteId = newIdType(parts.getResourceType(), parts.getResourceId()); + if (!deletedResources.contains(deleteId.getValueAsString())) { + DaoMethodOutcome outcome = dao.delete(deleteId, deleteConflicts, theRequestDetails); + if (outcome.getEntity() != null) { + deletedResources.add(deleteId.getValueAsString()); + entriesToProcess.put(nextRespEntry, outcome.getEntity()); + } + } } else { - matchUrl = parts.getResourceType(); - } - matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - outcome = resourceDao.update(res, matchUrl, false, theRequestDetails); - if (Boolean.TRUE.equals(outcome.getCreated())) { - conditionalRequestUrls.put(matchUrl, res.getClass()); - } - } + String matchUrl = parts.getResourceType() + '?' + parts.getParams(); + matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); + DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequestDetails); + List allDeleted = deleteOutcome.getDeletedEntities(); + for (ResourceTable deleted : allDeleted) { + deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString()); + } + if (allDeleted.isEmpty()) { + status = Constants.STATUS_HTTP_204_NO_CONTENT; + } - if (outcome.getCreated() == Boolean.FALSE) { - updatedEntities.add(outcome.getEntity()); - } + myVersionAdapter.setResponseOutcome(nextRespEntry, deleteOutcome.getOperationOutcome()); + } + + myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(status)); + + break; + } + case "PUT": { + // UPDATE + @SuppressWarnings("rawtypes") + IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); + + String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); + + DaoMethodOutcome outcome; + UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); + if (isNotBlank(parts.getResourceId())) { + String version = null; + if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) { + version = ParameterUtil.parseETagValue(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry)); + } + res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version)); + outcome = resourceDao.update(res, null, false, theRequestDetails); + } else { + res.setId((String) null); + String matchUrl; + if (isNotBlank(parts.getParams())) { + matchUrl = parts.getResourceType() + '?' + parts.getParams(); + } else { + matchUrl = parts.getResourceType(); + } + matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); + outcome = resourceDao.update(res, matchUrl, false, theRequestDetails); + if (Boolean.TRUE.equals(outcome.getCreated())) { + conditionalRequestUrls.put(matchUrl, res.getClass()); + } + } + + if (outcome.getCreated() == Boolean.FALSE) { + updatedEntities.add(outcome.getEntity()); + } + + handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); + entriesToProcess.put(nextRespEntry, outcome.getEntity()); + break; + } + case "GET": + default: + break; - handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); - entriesToProcess.put(nextRespEntry, outcome.getEntity()); - break; } - case "GET": - default: - break; + theTransactionStopWatch.endCurrentTask(); + } + + + /* + * Make sure that there are no conflicts from deletions. E.g. we can't delete something + * if something else has a reference to it.. Unless the thing that has a reference to it + * was also deleted as a part of this transaction, which is why we check this now at the + * end. + */ + + deleteConflicts.removeIf(next -> + deletedResources.contains(next.getTargetId().toUnqualifiedVersionless().getValue())); + myDao.validateDeleteConflictsEmptyOrThrowException(deleteConflicts); + + /* + * Perform ID substitutions and then index each resource we have saved + */ + + FhirTerser terser = myContext.newTerser(); + theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources"); + for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) { + IBaseResource nextResource = nextOutcome.getResource(); + if (nextResource == null) { + continue; + } + + // References + List allRefs = terser.getAllResourceReferences(nextResource); + for (ResourceReferenceInfo nextRef : allRefs) { + IIdType nextId = nextRef.getResourceReference().getReferenceElement(); + if (!nextId.hasIdPart()) { + continue; + } + if (theIdSubstitutions.containsKey(nextId)) { + IIdType newId = theIdSubstitutions.get(nextId); + ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); + nextRef.getResourceReference().setReference(newId.getValue()); + } else if (nextId.getValue().startsWith("urn:")) { + throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType()); + } else { + ourLog.debug(" * Reference [{}] does not exist in bundle", nextId); + } + } + + // URIs + Class> uriType = (Class>) myContext.getElementDefinition("uri").getImplementingClass(); + List> allUris = terser.getAllPopulatedChildElementsOfType(nextResource, uriType); + for (IPrimitiveType nextRef : allUris) { + if (nextRef instanceof IIdType) { + continue; // No substitution on the resource ID itself! + } + IIdType nextUriString = newIdType(nextRef.getValueAsString()); + if (theIdSubstitutions.containsKey(nextUriString)) { + IIdType newId = theIdSubstitutions.get(nextUriString); + ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); + nextRef.setValueAsString(newId.getValue()); + } else { + ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); + } + } + + IPrimitiveType deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource); + Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; + + if (updatedEntities.contains(nextOutcome.getEntity())) { + myDao.updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); + } else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) { + myDao.updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true); + } } theTransactionStopWatch.endCurrentTask(); - } + theTransactionStopWatch.startTask("Flush writes to database"); + flushJpaSession(); - /* - * Make sure that there are no conflicts from deletions. E.g. we can't delete something - * if something else has a reference to it.. Unless the thing that has a reference to it - * was also deleted as a part of this transaction, which is why we check this now at the - * end. - */ - - deleteConflicts.removeIf(next -> - deletedResources.contains(next.getTargetId().toUnqualifiedVersionless().getValue())); - myDao.validateDeleteConflictsEmptyOrThrowException(deleteConflicts); - - /* - * Perform ID substitutions and then index each resource we have saved - */ - - FhirTerser terser = myContext.newTerser(); - theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources"); - for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) { - IBaseResource nextResource = nextOutcome.getResource(); - if (nextResource == null) { - continue; + theTransactionStopWatch.endCurrentTask(); + if (conditionalRequestUrls.size() > 0) { + theTransactionStopWatch.startTask("Check for conflicts in conditional resources"); } - // References - List allRefs = terser.getAllResourceReferences(nextResource); - for (ResourceReferenceInfo nextRef : allRefs) { - IIdType nextId = nextRef.getResourceReference().getReferenceElement(); - if (!nextId.hasIdPart()) { + /* + * Double check we didn't allow any duplicates we shouldn't have + */ + for (Map.Entry> nextEntry : conditionalRequestUrls.entrySet()) { + String matchUrl = nextEntry.getKey(); + Class resType = nextEntry.getValue(); + if (isNotBlank(matchUrl)) { + IFhirResourceDao resourceDao = myDao.getDao(resType); + Set val = resourceDao.processMatchUrl(matchUrl); + if (val.size() > 1) { + throw new InvalidRequestException( + "Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?"); + } + } + } + + theTransactionStopWatch.endCurrentTask(); + + for (IIdType next : theAllIds) { + IIdType replacement = theIdSubstitutions.get(next); + if (replacement == null) { continue; } - if (theIdSubstitutions.containsKey(nextId)) { - IIdType newId = theIdSubstitutions.get(nextId); - ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); - nextRef.getResourceReference().setReference(newId.getValue()); - } else if (nextId.getValue().startsWith("urn:")) { - throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType()); - } else { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextId); + if (replacement.equals(next)) { + continue; } + ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); } + return entriesToProcess; - // URIs - Class> uriType = (Class>) myContext.getElementDefinition("uri").getImplementingClass(); - List> allUris = terser.getAllPopulatedChildElementsOfType(nextResource, uriType); - for (IPrimitiveType nextRef : allUris) { - if (nextRef instanceof IIdType) { - continue; // No substitution on the resource ID itself! - } - IIdType nextUriString = newIdType(nextRef.getValueAsString()); - if (theIdSubstitutions.containsKey(nextUriString)) { - IIdType newId = theIdSubstitutions.get(nextUriString); - ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); - nextRef.setValueAsString(newId.getValue()); - } else { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); - } - } - - IPrimitiveType deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource); - Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; - - if (updatedEntities.contains(nextOutcome.getEntity())) { - myDao.updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); - } else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) { - myDao.updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true); + } finally { + if (theRequestDetails != null) { + theRequestDetails.stopDeferredRequestOperationCallbackAndRunDeferredItems(); } } - - theTransactionStopWatch.endCurrentTask(); - theTransactionStopWatch.startTask("Flush writes to database"); - - flushJpaSession(); - - theTransactionStopWatch.endCurrentTask(); - if (conditionalRequestUrls.size() > 0) { - theTransactionStopWatch.startTask("Check for conflicts in conditional resources"); - } - - /* - * Double check we didn't allow any duplicates we shouldn't have - */ - for (Map.Entry> nextEntry : conditionalRequestUrls.entrySet()) { - String matchUrl = nextEntry.getKey(); - Class resType = nextEntry.getValue(); - if (isNotBlank(matchUrl)) { - IFhirResourceDao resourceDao = myDao.getDao(resType); - Set val = resourceDao.processMatchUrl(matchUrl); - if (val.size() > 1) { - throw new InvalidRequestException( - "Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?"); - } - } - } - - theTransactionStopWatch.endCurrentTask(); - - for (IIdType next : theAllIds) { - IIdType replacement = theIdSubstitutions.get(next); - if (replacement == null) { - continue; - } - if (replacement.equals(next)) { - continue; - } - ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); - } - return entriesToProcess; } private IIdType newIdType(String theResourceType, String theResourceId, String theVersion) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index 73136b730b6..e726ef99b2f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -25,5 +25,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import ca.uhn.fhir.jpa.entity.ResourceLink; public interface IResourceLinkDao extends JpaRepository { - // nothing + + + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceReindexJobDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceReindexJobDao.java index fcc4c4270ee..66091249dd4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceReindexJobDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceReindexJobDao.java @@ -10,6 +10,7 @@ import org.springframework.data.repository.query.Param; import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Optional; /* * #%L @@ -55,4 +56,12 @@ public interface IResourceReindexJobDao extends JpaRepository getReindexCount(@Param("id") Long theId); + + @Query("UPDATE ResourceReindexJobEntity j SET j.myReindexCount = :newCount WHERE j.myId = :id") + @Modifying + void setReindexCount(@Param("id") Long theId, @Param("newCount") int theNewCount); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCodeSystemDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCodeSystemDstu3.java index 29cfecb2940..8bd2adeacfd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCodeSystemDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCodeSystemDstu3.java @@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystem; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; -import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.jpa.util.LogicUtil; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenParam; @@ -65,9 +64,6 @@ public class FhirResourceDaoCodeSystemDstu3 extends FhirResourceDaoDstu3 translateForcedIdToPids(IIdType theId) { + return IdHelperService.translateForcedIdToPids(myDaoConfig, theId, myForcedIdDao); + } + + static List translateForcedIdToPids(DaoConfig theDaoConfig, IIdType theId, IForcedIdDao theForcedIdDao) { + Validate.isTrue(theId.hasIdPart()); + + if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(theId)) { + return Collections.singletonList(theId.getIdPartAsLong()); + } else { + List forcedId; + if (theId.hasResourceType()) { + forcedId = theForcedIdDao.findByTypeAndForcedId(theId.getResourceType(), theId.getIdPart()); + } else { + forcedId = theForcedIdDao.findByForcedId(theId.getIdPart()); + } + + if (!forcedId.isEmpty()) { + List retVal = new ArrayList<>(forcedId.size()); + for (ForcedId next : forcedId) { + retVal.add(next.getResourcePid()); + } + return retVal; + } else { + throw new ResourceNotFoundException(theId); + } + } + } + + public String translatePidIdToForcedId(String theResourceType, Long theId) { + ForcedId forcedId = myForcedIdDao.findByResourcePid(theId); + if (forcedId != null) { + return forcedId.getResourceType() + '/' + forcedId.getForcedId(); + } else { + return theResourceType + '/' + theId.toString(); + } + } + + public static boolean isValidPid(IIdType theId) { + if (theId == null || theId.getIdPart() == null) { + return false; + } + String idPart = theId.getIdPart(); + for (int i = 0; i < idPart.length(); i++) { + char nextChar = idPart.charAt(i); + if (nextChar < '0' || nextChar > '9') { + return false; + } + } + return true; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IndexingSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IndexingSupport.java deleted file mode 100644 index 4c121819f4c..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IndexingSupport.java +++ /dev/null @@ -1,58 +0,0 @@ -package ca.uhn.fhir.jpa.dao.index; - -/*- - * #%L - * HAPI FHIR JPA Server - * %% - * Copyright (C) 2014 - 2018 University Health Network - * %% - * 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.Collection; -import java.util.Map; -import java.util.Set; - -import javax.persistence.EntityManager; - -import ca.uhn.fhir.jpa.entity.BaseHasResource; -import ca.uhn.fhir.jpa.entity.IBaseResourceEntity; -import ca.uhn.fhir.jpa.entity.ResourceTag; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.ISearchParamExtractor; -import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; -import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao; - -public interface IndexingSupport { - public DaoConfig getConfig(); - public ISearchParamExtractor getSearchParamExtractor(); - public ISearchParamRegistry getSearchParamRegistry(); - public FhirContext getContext(); - public EntityManager getEntityManager(); - public IFhirResourceDao getDao(Class theType); - public Map, IFhirResourceDao> getResourceTypeToDao(); - public boolean isLogicalReference(IIdType nextId); - public IForcedIdDao getForcedIdDao(); - public Set processMatchUrl(String theMatchUrl, Class theResourceType); - public Long translateForcedIdToPid(String theResourceName, String theResourceId); - public String toResourceName(Class theResourceType); - public IResourceIndexedCompositeStringUniqueDao getResourceIndexedCompositeStringUniqueDao(); - -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/ResourceIndexedSearchParams.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/ResourceIndexedSearchParams.java index b04467704a0..6f4adbf9f08 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/ResourceIndexedSearchParams.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/ResourceIndexedSearchParams.java @@ -20,334 +20,78 @@ package ca.uhn.fhir.jpa.dao.index; * #L% */ -import static org.apache.commons.lang3.StringUtils.compare; -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import javax.persistence.EntityManager; - -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IBaseExtension; -import org.hl7.fhir.instance.model.api.IBaseReference; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.CanonicalType; -import org.hl7.fhir.r4.model.Reference; - -import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.PathAndRef; -import ca.uhn.fhir.jpa.entity.BaseResourceIndexedSearchParam; -import ca.uhn.fhir.jpa.entity.ForcedId; -import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamCoords; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamNumber; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamQuantity; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri; -import ca.uhn.fhir.jpa.entity.ResourceLink; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.util.FhirTerser; -import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.rest.param.ReferenceParam; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.Map.Entry; +import java.util.function.Predicate; + +import static org.apache.commons.lang3.StringUtils.*; public class ResourceIndexedSearchParams { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceIndexedSearchParams.class); - // FIXME rename - private final IndexingSupport myIndexingService; - - private final Collection stringParams; - private final Collection tokenParams; - private final Collection numberParams; - private final Collection quantityParams; - private final Collection dateParams; - private final Collection uriParams; - private final Collection coordsParams; - private final Collection compositeStringUniques; - private final Collection links; - - private Set populatedResourceLinkParameters = Collections.emptySet(); - - public ResourceIndexedSearchParams(IndexingSupport indexingService, ResourceTable theEntity) { - this.myIndexingService = indexingService; + final Collection stringParams = new ArrayList<>(); + final Collection tokenParams = new HashSet<>(); + final Collection numberParams = new ArrayList<>(); + final Collection quantityParams = new ArrayList<>(); + final Collection dateParams = new ArrayList<>(); + final Collection uriParams = new ArrayList<>(); + final Collection coordsParams = new ArrayList<>(); - stringParams = new ArrayList<>(); + final Collection compositeStringUniques = new HashSet<>(); + final Collection links = new HashSet<>(); + final Set populatedResourceLinkParameters = new HashSet<>(); + + + public ResourceIndexedSearchParams() { + } + + public ResourceIndexedSearchParams(ResourceTable theEntity) { if (theEntity.isParamsStringPopulated()) { stringParams.addAll(theEntity.getParamsString()); } - tokenParams = new ArrayList<>(); if (theEntity.isParamsTokenPopulated()) { tokenParams.addAll(theEntity.getParamsToken()); } - numberParams = new ArrayList<>(); if (theEntity.isParamsNumberPopulated()) { numberParams.addAll(theEntity.getParamsNumber()); } - quantityParams = new ArrayList<>(); if (theEntity.isParamsQuantityPopulated()) { quantityParams.addAll(theEntity.getParamsQuantity()); } - dateParams = new ArrayList<>(); if (theEntity.isParamsDatePopulated()) { dateParams.addAll(theEntity.getParamsDate()); } - uriParams = new ArrayList<>(); if (theEntity.isParamsUriPopulated()) { uriParams.addAll(theEntity.getParamsUri()); } - coordsParams = new ArrayList<>(); if (theEntity.isParamsCoordsPopulated()) { coordsParams.addAll(theEntity.getParamsCoords()); } - links = new ArrayList<>(); if (theEntity.isHasLinks()) { links.addAll(theEntity.getResourceLinks()); } - compositeStringUniques = new ArrayList<>(); if (theEntity.isParamsCompositeStringUniquePresent()) { compositeStringUniques.addAll(theEntity.getParamsCompositeStringUnique()); } } - public ResourceIndexedSearchParams(IndexingSupport indexingService) { - this.myIndexingService = indexingService; - stringParams = Collections.emptySet(); - tokenParams = Collections.emptySet(); - numberParams = Collections.emptySet(); - quantityParams = Collections.emptySet(); - dateParams = Collections.emptySet(); - uriParams = Collections.emptySet(); - coordsParams = Collections.emptySet(); - links = Collections.emptySet(); - compositeStringUniques = Collections.emptySet(); - } - - public ResourceIndexedSearchParams(IndexingSupport indexingService, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams existingParams) { - this.myIndexingService = indexingService; - - stringParams = extractSearchParamStrings(theEntity, theResource); - numberParams = extractSearchParamNumber(theEntity, theResource); - quantityParams = extractSearchParamQuantity(theEntity, theResource); - dateParams = extractSearchParamDates(theEntity, theResource); - uriParams = extractSearchParamUri(theEntity, theResource); - coordsParams = extractSearchParamCoords(theEntity, theResource); - - ourLog.trace("Storing date indexes: {}", dateParams); - - tokenParams = new HashSet<>(); - for (BaseResourceIndexedSearchParam next : extractSearchParamTokens(theEntity, theResource)) { - if (next instanceof ResourceIndexedSearchParamToken) { - tokenParams.add((ResourceIndexedSearchParamToken) next); - } else { - stringParams.add((ResourceIndexedSearchParamString) next); - } - } - - Set> activeSearchParams = myIndexingService.getSearchParamRegistry().getActiveSearchParams(theEntity.getResourceType()).entrySet(); - DaoConfig myConfig = indexingService.getConfig(); - if (myConfig .getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) { - findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.STRING, stringParams); - findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.NUMBER, numberParams); - findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.QUANTITY, quantityParams); - findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.DATE, dateParams); - findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.URI, uriParams); - findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.TOKEN, tokenParams); - } - - setUpdatedTime(stringParams, theUpdateTime); - setUpdatedTime(numberParams, theUpdateTime); - setUpdatedTime(quantityParams, theUpdateTime); - setUpdatedTime(dateParams, theUpdateTime); - setUpdatedTime(uriParams, theUpdateTime); - setUpdatedTime(coordsParams, theUpdateTime); - setUpdatedTime(tokenParams, theUpdateTime); - - /* - * Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the - * matching resource. - */ - if (myConfig.isAllowInlineMatchUrlReferences()) { - FhirTerser terser = myIndexingService.getContext().newTerser(); - List allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class); - for (IBaseReference nextRef : allRefs) { - IIdType nextId = nextRef.getReferenceElement(); - String nextIdText = nextId.getValue(); - if (nextIdText == null) { - continue; - } - int qmIndex = nextIdText.indexOf('?'); - if (qmIndex != -1) { - for (int i = qmIndex - 1; i >= 0; i--) { - if (nextIdText.charAt(i) == '/') { - if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') { - // Just in case the URL is in the form Patient/?foo=bar - continue; - } - nextIdText = nextIdText.substring(i + 1); - break; - } - } - String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", ""); - RuntimeResourceDefinition matchResourceDef = myIndexingService.getContext().getResourceDefinition(resourceTypeString); - if (matchResourceDef == null) { - String msg = myIndexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString); - throw new InvalidRequestException(msg); - } - Class matchResourceType = matchResourceDef.getImplementingClass(); - Set matches = myIndexingService.processMatchUrl(nextIdText, matchResourceType); - if (matches.isEmpty()) { - String msg = indexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue()); - throw new ResourceNotFoundException(msg); - } - if (matches.size() > 1) { - String msg = indexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue()); - throw new PreconditionFailedException(msg); - } - Long next = matches.iterator().next(); - String newId = translatePidIdToForcedId(resourceTypeString, next); - ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId); - nextRef.setReference(newId); - } - } - } - - links = new HashSet<>(); - populatedResourceLinkParameters = extractResourceLinks(theEntity, theResource, links, theUpdateTime); - - /* - * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them - */ - for (Iterator existingLinkIter = existingParams.getResourceLinks().iterator(); existingLinkIter.hasNext(); ) { - ResourceLink nextExisting = existingLinkIter.next(); - if (links.remove(nextExisting)) { - existingLinkIter.remove(); - links.add(nextExisting); - } - } - - /* - * Handle composites - */ - compositeStringUniques = extractCompositeStringUniques(theEntity, stringParams, tokenParams, numberParams, quantityParams, dateParams, uriParams, links); - - - } public Collection getResourceLinks() { return links; } - - - protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { - return myIndexingService.getSearchParamExtractor().extractSearchParamCoords(theEntity, theResource); - } - - protected Set extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) { - return myIndexingService.getSearchParamExtractor().extractSearchParamDates(theEntity, theResource); - } - - protected Set extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) { - return myIndexingService.getSearchParamExtractor().extractSearchParamNumber(theEntity, theResource); - } - - protected Set extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) { - return myIndexingService.getSearchParamExtractor().extractSearchParamQuantity(theEntity, theResource); - } - - protected Set extractSearchParamStrings(ResourceTable theEntity, IBaseResource theResource) { - return myIndexingService.getSearchParamExtractor().extractSearchParamStrings(theEntity, theResource); - } - - protected Set extractSearchParamTokens(ResourceTable theEntity, IBaseResource theResource) { - return myIndexingService.getSearchParamExtractor().extractSearchParamTokens(theEntity, theResource); - } - - protected Set extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) { - return myIndexingService.getSearchParamExtractor().extractSearchParamUri(theEntity, theResource); - } - - @SuppressWarnings("unchecked") - private void findMissingSearchParams(ResourceTable theEntity, Set> activeSearchParams, RestSearchParameterTypeEnum type, - Collection paramCollection) { - for (Entry nextEntry : activeSearchParams) { - String nextParamName = nextEntry.getKey(); - if (nextEntry.getValue().getParamType() == type) { - boolean haveParam = false; - for (BaseResourceIndexedSearchParam nextParam : paramCollection) { - if (nextParam.getParamName().equals(nextParamName)) { - haveParam = true; - break; - } - } - - if (!haveParam) { - BaseResourceIndexedSearchParam param; - switch (type) { - case DATE: - param = new ResourceIndexedSearchParamDate(); - break; - case NUMBER: - param = new ResourceIndexedSearchParamNumber(); - break; - case QUANTITY: - param = new ResourceIndexedSearchParamQuantity(); - break; - case STRING: - param = new ResourceIndexedSearchParamString() - .setDaoConfig(myIndexingService.getConfig()); - break; - case TOKEN: - param = new ResourceIndexedSearchParamToken(); - break; - case URI: - param = new ResourceIndexedSearchParamUri(); - break; - case COMPOSITE: - case HAS: - case REFERENCE: - default: - continue; - } - param.setResource(theEntity); - param.setMissing(true); - param.setParamName(nextParamName); - paramCollection.add((RT) param); - } - } - } - } - - public void setParams(ResourceTable theEntity) { + public void setParamsOn(ResourceTable theEntity) { theEntity.setParamsString(stringParams); theEntity.setParamsStringPopulated(stringParams.isEmpty() == false); theEntity.setParamsToken(tokenParams); @@ -367,89 +111,23 @@ public class ResourceIndexedSearchParams { theEntity.setHasLinks(links.isEmpty() == false); } - - private Set extractCompositeStringUniques(ResourceTable theEntity, Collection theStringParams, Collection theTokenParams, Collection theNumberParams, Collection theQuantityParams, Collection theDateParams, Collection theUriParams, Collection theLinks) { - Set compositeStringUniques; - compositeStringUniques = new HashSet<>(); - List uniqueSearchParams = myIndexingService.getSearchParamRegistry().getActiveUniqueSearchParams(theEntity.getResourceType()); - for (JpaRuntimeSearchParam next : uniqueSearchParams) { - - List> partsChoices = new ArrayList<>(); - - for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) { - Collection paramsListForCompositePart = null; - Collection linksForCompositePart = null; - Collection linksForCompositePartWantPaths = null; - switch (nextCompositeOf.getParamType()) { - case NUMBER: - paramsListForCompositePart = theNumberParams; - break; - case DATE: - paramsListForCompositePart = theDateParams; - break; - case STRING: - paramsListForCompositePart = theStringParams; - break; - case TOKEN: - paramsListForCompositePart = theTokenParams; - break; - case REFERENCE: - linksForCompositePart = theLinks; - linksForCompositePartWantPaths = new HashSet<>(); - linksForCompositePartWantPaths.addAll(nextCompositeOf.getPathsSplit()); - break; - case QUANTITY: - paramsListForCompositePart = theQuantityParams; - break; - case URI: - paramsListForCompositePart = theUriParams; - break; - case COMPOSITE: - case HAS: - break; - } - - ArrayList nextChoicesList = new ArrayList<>(); - partsChoices.add(nextChoicesList); - - String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName()); - if (paramsListForCompositePart != null) { - for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) { - if (nextParam.getParamName().equals(nextCompositeOf.getName())) { - IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType(); - String value = nextParamAsClientParam.getValueAsQueryToken(myIndexingService.getContext()); - if (isNotBlank(value)) { - value = UrlUtil.escapeUrlParam(value); - nextChoicesList.add(key + "=" + value); - } - } - } - } - if (linksForCompositePart != null) { - for (ResourceLink nextLink : linksForCompositePart) { - if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) { - String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue(); - if (isNotBlank(value)) { - value = UrlUtil.escapeUrlParam(value); - nextChoicesList.add(key + "=" + value); - } - } - } - } - } - - Set queryStringsToPopulate = extractCompositeStringUniquesValueChains(theEntity.getResourceType(), partsChoices); - - for (String nextQueryString : queryStringsToPopulate) { - if (isNotBlank(nextQueryString)) { - compositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString)); - } - } - } - - return compositeStringUniques; + public void setUpdatedTime(Date theUpdateTime) { + setUpdatedTime(stringParams, theUpdateTime); + setUpdatedTime(numberParams, theUpdateTime); + setUpdatedTime(quantityParams, theUpdateTime); + setUpdatedTime(dateParams, theUpdateTime); + setUpdatedTime(uriParams, theUpdateTime); + setUpdatedTime(coordsParams, theUpdateTime); + setUpdatedTime(tokenParams, theUpdateTime); } - + + private void setUpdatedTime(Collection theParams, Date theUpdateTime) { + for (BaseResourceIndexedSearchParam nextSearchParam : theParams) { + nextSearchParam.setUpdated(theUpdateTime); + } + } + + /** * This method is used to create a set of all possible combinations of * parameters across a set of search parameters. An example of why @@ -531,342 +209,163 @@ public class ResourceIndexedSearchParams { } - /** - * @return Returns a set containing all of the parameter names that - * were found to have a value - */ - @SuppressWarnings("unchecked") - protected Set extractResourceLinks(ResourceTable theEntity, IBaseResource theResource, Collection theLinks, Date theUpdateTime) { - HashSet retVal = new HashSet<>(); - String resourceType = theEntity.getResourceType(); - /* - * For now we don't try to load any of the links in a bundle if it's the actual bundle we're storing.. - */ - if (theResource instanceof IBaseBundle) { - return Collections.emptySet(); - } - - Map searchParams = myIndexingService.getSearchParamRegistry().getActiveSearchParams(myIndexingService.toResourceName(theResource.getClass())); - for (RuntimeSearchParam nextSpDef : searchParams.values()) { - - if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { - continue; - } - - String nextPathsUnsplit = nextSpDef.getPath(); - if (isBlank(nextPathsUnsplit)) { - continue; - } - - boolean multiType = false; - if (nextPathsUnsplit.endsWith("[x]")) { - multiType = true; - } - - List refs = myIndexingService.getSearchParamExtractor().extractResourceLinks(theResource, nextSpDef); - for (PathAndRef nextPathAndRef : refs) { - Object nextObject = nextPathAndRef.getRef(); - - /* - * A search parameter on an extension field that contains - * references should index those references - */ - if (nextObject instanceof IBaseExtension) { - nextObject = ((IBaseExtension) nextObject).getValue(); - } - - if (nextObject instanceof CanonicalType) { - nextObject = new Reference(((CanonicalType) nextObject).getValueAsString()); - } - - IIdType nextId; - if (nextObject instanceof IBaseReference) { - IBaseReference nextValue = (IBaseReference) nextObject; - if (nextValue.isEmpty()) { - continue; - } - nextId = nextValue.getReferenceElement(); - - /* - * This can only really happen if the DAO is being called - * programatically with a Bundle (not through the FHIR REST API) - * but Smile does this - */ - if (nextId.isEmpty() && nextValue.getResource() != null) { - nextId = nextValue.getResource().getIdElement(); - } - - if (nextId.isEmpty() || nextId.getValue().startsWith("#")) { - // This is a blank or contained resource reference - continue; - } - } else if (nextObject instanceof IBaseResource) { - nextId = ((IBaseResource) nextObject).getIdElement(); - if (nextId == null || nextId.hasIdPart() == false) { - continue; - } - } else if (myIndexingService.getContext().getElementDefinition((Class) nextObject.getClass()).getName().equals("uri")) { - continue; - } else if (resourceType.equals("Consent") && nextPathAndRef.getPath().equals("Consent.source")) { - // Consent#source-identifier has a path that isn't typed - This is a one-off to deal with that - continue; - } else { - if (!multiType) { - if (nextSpDef.getName().equals("sourceuri")) { - continue; // TODO: disable this eventually - ConceptMap:sourceuri is of type reference but points to a URI - } - throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass()); - } else { - continue; - } - } - - retVal.add(nextSpDef.getName()); - - if (myIndexingService.isLogicalReference(nextId)) { - ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime); - if (theLinks.add(resourceLink)) { - ourLog.debug("Indexing remote resource reference URL: {}", nextId); - } - continue; - } - - String baseUrl = nextId.getBaseUrl(); - String typeString = nextId.getResourceType(); - if (isBlank(typeString)) { - throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue()); - } - RuntimeResourceDefinition resourceDefinition; - try { - resourceDefinition = myIndexingService.getContext().getResourceDefinition(typeString); - } catch (DataFormatException e) { - throw new InvalidRequestException( - "Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue()); - } - - if (isNotBlank(baseUrl)) { - if (!myIndexingService.getConfig().getTreatBaseUrlsAsLocal().contains(baseUrl) && !myIndexingService.getConfig().isAllowExternalReferences()) { - String msg = myIndexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "externalReferenceNotAllowed", nextId.getValue()); - throw new InvalidRequestException(msg); - } else { - ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime); - if (theLinks.add(resourceLink)) { - ourLog.debug("Indexing remote resource reference URL: {}", nextId); - } - continue; - } - } - - Class type = resourceDefinition.getImplementingClass(); - String id = nextId.getIdPart(); - if (StringUtils.isBlank(id)) { - throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue()); - } - - IFhirResourceDao dao = myIndexingService.getDao(type); - if (dao == null) { - StringBuilder b = new StringBuilder(); - b.append("This server (version "); - b.append(myIndexingService.getContext().getVersion().getVersion()); - b.append(") is not able to handle resources of type["); - b.append(nextId.getResourceType()); - b.append("] - Valid resource types for this server: "); - b.append(myIndexingService.getResourceTypeToDao().keySet().toString()); - - throw new InvalidRequestException(b.toString()); - } - Long valueOf; - try { - valueOf = myIndexingService.translateForcedIdToPid(typeString, id); - } catch (ResourceNotFoundException e) { - if (myIndexingService.getConfig().isEnforceReferentialIntegrityOnWrite() == false) { - continue; - } - RuntimeResourceDefinition missingResourceDef = myIndexingService.getContext().getResourceDefinition(type); - String resName = missingResourceDef.getName(); - - if (myIndexingService.getConfig().isAutoCreatePlaceholderReferenceTargets()) { - IBaseResource newResource = missingResourceDef.newInstance(); - newResource.setId(resName + "/" + id); - IFhirResourceDao placeholderResourceDao = (IFhirResourceDao) myIndexingService.getDao(newResource.getClass()); - ourLog.debug("Automatically creating empty placeholder resource: {}", newResource.getIdElement().getValue()); - valueOf = placeholderResourceDao.update(newResource).getEntity().getId(); - } else { - throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit); - } - } - ResourceTable target = myIndexingService.getEntityManager().find(ResourceTable.class, valueOf); - RuntimeResourceDefinition targetResourceDef = myIndexingService.getContext().getResourceDefinition(type); - if (target == null) { - String resName = targetResourceDef.getName(); - throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit); - } - - if (!typeString.equals(target.getResourceType())) { - throw new UnprocessableEntityException( - "Resource contains reference to " + nextId.getValue() + " but resource with ID " + nextId.getIdPart() + " is actually of type " + target.getResourceType()); - } - - if (target.getDeleted() != null) { - String resName = targetResourceDef.getName(); - throw new InvalidRequestException("Resource " + resName + "/" + id + " is deleted, specified in path: " + nextPathsUnsplit); - } - - if (nextSpDef.getTargets() != null && !nextSpDef.getTargets().contains(typeString)) { - continue; - } - - ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, target, theUpdateTime); - theLinks.add(resourceLink); - } - - } - - theEntity.setHasLinks(theLinks.size() > 0); - - return retVal; - } - - private void setUpdatedTime(Collection theParams, Date theUpdateTime) { - for (BaseResourceIndexedSearchParam nextSearchParam : theParams) { - nextSearchParam.setUpdated(theUpdateTime); - } - } - - private String translatePidIdToForcedId(String theResourceType, Long theId) { - ForcedId forcedId = myIndexingService.getForcedIdDao().findByResourcePid(theId); - if (forcedId != null) { - return forcedId.getResourceType() + '/' + forcedId.getForcedId(); - } else { - return theResourceType + '/' + theId.toString(); - } - } - - public void removeCommon(ResourceTable theEntity, ResourceIndexedSearchParams existingParams) { - EntityManager myEntityManager = myIndexingService.getEntityManager(); - - calculateHashes(stringParams); - for (ResourceIndexedSearchParamString next : removeCommon(existingParams.stringParams, stringParams)) { - next.setDaoConfig(myIndexingService.getConfig()); - myEntityManager .remove(next); - theEntity.getParamsString().remove(next); - } - for (ResourceIndexedSearchParamString next : removeCommon(stringParams, existingParams.stringParams)) { - myEntityManager.persist(next); - } - - calculateHashes(tokenParams); - for (ResourceIndexedSearchParamToken next : removeCommon(existingParams.tokenParams, tokenParams)) { - myEntityManager.remove(next); - theEntity.getParamsToken().remove(next); - } - for (ResourceIndexedSearchParamToken next : removeCommon(tokenParams, existingParams.tokenParams)) { - myEntityManager.persist(next); - } - - calculateHashes(numberParams); - for (ResourceIndexedSearchParamNumber next : removeCommon(existingParams.numberParams, numberParams)) { - myEntityManager.remove(next); - theEntity.getParamsNumber().remove(next); - } - for (ResourceIndexedSearchParamNumber next : removeCommon(numberParams, existingParams.numberParams)) { - myEntityManager.persist(next); - } - - calculateHashes(quantityParams); - for (ResourceIndexedSearchParamQuantity next : removeCommon(existingParams.quantityParams, quantityParams)) { - myEntityManager.remove(next); - theEntity.getParamsQuantity().remove(next); - } - for (ResourceIndexedSearchParamQuantity next : removeCommon(quantityParams, existingParams.quantityParams)) { - myEntityManager.persist(next); - } - - // Store date SP's - calculateHashes(dateParams); - for (ResourceIndexedSearchParamDate next : removeCommon(existingParams.dateParams, dateParams)) { - myEntityManager.remove(next); - theEntity.getParamsDate().remove(next); - } - for (ResourceIndexedSearchParamDate next : removeCommon(dateParams, existingParams.dateParams)) { - myEntityManager.persist(next); - } - - // Store URI SP's - calculateHashes(uriParams); - for (ResourceIndexedSearchParamUri next : removeCommon(existingParams.uriParams, uriParams)) { - myEntityManager.remove(next); - theEntity.getParamsUri().remove(next); - } - for (ResourceIndexedSearchParamUri next : removeCommon(uriParams, existingParams.uriParams)) { - myEntityManager.persist(next); - } - - // Store Coords SP's - calculateHashes(coordsParams); - for (ResourceIndexedSearchParamCoords next : removeCommon(existingParams.coordsParams, coordsParams)) { - myEntityManager.remove(next); - theEntity.getParamsCoords().remove(next); - } - for (ResourceIndexedSearchParamCoords next : removeCommon(coordsParams, existingParams.coordsParams)) { - myEntityManager.persist(next); - } - - // Store resource links - for (ResourceLink next : removeCommon(existingParams.links, links)) { - myEntityManager.remove(next); - theEntity.getResourceLinks().remove(next); - } - for (ResourceLink next : removeCommon(links, existingParams.links)) { - myEntityManager.persist(next); - } - - // make sure links are indexed - theEntity.setResourceLinks(links); - - // Store composite string uniques - if (myIndexingService.getConfig().isUniqueIndexesEnabled()) { - for (ResourceIndexedCompositeStringUnique next : removeCommon(existingParams.compositeStringUniques, compositeStringUniques)) { - ourLog.debug("Removing unique index: {}", next); - myEntityManager.remove(next); - theEntity.getParamsCompositeStringUnique().remove(next); - } - for (ResourceIndexedCompositeStringUnique next : removeCommon(compositeStringUniques, existingParams.compositeStringUniques)) { - if (myIndexingService.getConfig().isUniqueIndexesCheckedBeforeSave()) { - ResourceIndexedCompositeStringUnique existing = myIndexingService.getResourceIndexedCompositeStringUniqueDao().findByQueryString(next.getIndexString()); - if (existing != null) { - String msg = myIndexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue()); - throw new PreconditionFailedException(msg); - } - } - ourLog.debug("Persisting unique index: {}", next); - myEntityManager.persist(next); - } - } - } - - private void calculateHashes(Collection theStringParams) { + void calculateHashes(Collection theStringParams) { for (BaseResourceIndexedSearchParam next : theStringParams) { next.calculateHashes(); } } - - private Collection removeCommon(Collection theInput, Collection theToRemove) { - assert theInput != theToRemove; - - if (theInput.isEmpty()) { - return theInput; - } - - ArrayList retVal = new ArrayList<>(theInput); - retVal.removeAll(theToRemove); - return retVal; - } public Set getPopulatedResourceLinkParameters() { return populatedResourceLinkParameters; } + public boolean matchParam(String theResourceName, String theParamName, RuntimeSearchParam paramDef, IQueryParameterType theParam) { + if (paramDef == null) { + return false; + } + Collection resourceParams; + switch (paramDef.getParamType()) { + case TOKEN: + resourceParams = tokenParams; + break; + case QUANTITY: + resourceParams = quantityParams; + break; + case STRING: + resourceParams = stringParams; + break; + case NUMBER: + resourceParams = numberParams; + break; + case URI: + resourceParams = uriParams; + break; + case DATE: + resourceParams = dateParams; + break; + case REFERENCE: + return matchResourceLinks(theResourceName, theParamName, theParam); + case COMPOSITE: + case HAS: + case SPECIAL: + default: + resourceParams = null; + } + if (resourceParams == null) { + return false; + } + Predicate namedParamPredicate = param -> + param.getParamName().equalsIgnoreCase(theParamName) && + param.matches(theParam); + + return resourceParams.stream().anyMatch(namedParamPredicate); + } + + private boolean matchResourceLinks(String theResourceName, String theParamName, IQueryParameterType theParam) { + ReferenceParam reference = (ReferenceParam)theParam; + + Predicate namedParamPredicate = resourceLink -> + resourceLinkMatches(theResourceName, resourceLink, theParamName) + && resourceIdMatches(resourceLink, reference); + + return links.stream().anyMatch(namedParamPredicate); + } + + private boolean resourceIdMatches(ResourceLink theResourceLink, ReferenceParam theReference) { + ResourceTable target = theResourceLink.getTargetResource(); + IdDt idDt = target.getIdDt(); + if (idDt.isIdPartValidLong()) { + return theReference.getIdPartAsLong() == idDt.getIdPartAsLong(); + } else { + ForcedId forcedId = target.getForcedId(); + if (forcedId != null) { + return forcedId.getForcedId().equals(theReference.getValue()); + } else { + return false; + } + } + } + + private boolean resourceLinkMatches(String theResourceName, ResourceLink theResourceLink, String theParamName) { + return theResourceLink.getTargetResource().getResourceType().equalsIgnoreCase(theParamName) || + theResourceLink.getSourcePath().equalsIgnoreCase(theResourceName+"."+theParamName); + } + + @Override + public String toString() { + return "ResourceIndexedSearchParams{" + + "stringParams=" + stringParams + + ", tokenParams=" + tokenParams + + ", numberParams=" + numberParams + + ", quantityParams=" + quantityParams + + ", dateParams=" + dateParams + + ", uriParams=" + uriParams + + ", coordsParams=" + coordsParams + + ", compositeStringUniques=" + compositeStringUniques + + ", links=" + links + + '}'; + } + + void findMissingSearchParams(DaoConfig theDaoConfig, ResourceTable theEntity, Set> theActiveSearchParams) { + findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.STRING, stringParams); + findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.NUMBER, numberParams); + findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.QUANTITY, quantityParams); + findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.DATE, dateParams); + findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.URI, uriParams); + findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.TOKEN, tokenParams); + } + + @SuppressWarnings("unchecked") + private void findMissingSearchParams(DaoConfig theDaoConfig, ResourceTable theEntity, Set> activeSearchParams, RestSearchParameterTypeEnum type, + Collection paramCollection) { + for (Map.Entry nextEntry : activeSearchParams) { + String nextParamName = nextEntry.getKey(); + if (nextEntry.getValue().getParamType() == type) { + boolean haveParam = false; + for (BaseResourceIndexedSearchParam nextParam : paramCollection) { + if (nextParam.getParamName().equals(nextParamName)) { + haveParam = true; + break; + } + } + + if (!haveParam) { + BaseResourceIndexedSearchParam param; + switch (type) { + case DATE: + param = new ResourceIndexedSearchParamDate(); + break; + case NUMBER: + param = new ResourceIndexedSearchParamNumber(); + break; + case QUANTITY: + param = new ResourceIndexedSearchParamQuantity(); + break; + case STRING: + param = new ResourceIndexedSearchParamString() + .setDaoConfig(theDaoConfig); + break; + case TOKEN: + param = new ResourceIndexedSearchParamToken(); + break; + case URI: + param = new ResourceIndexedSearchParamUri(); + break; + case COMPOSITE: + case HAS: + case REFERENCE: + default: + continue; + } + param.setResource(theEntity); + param.setMissing(true); + param.setParamName(nextParamName); + paramCollection.add((RT) param); + } + } + } + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamExtractorService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamExtractorService.java new file mode 100644 index 00000000000..35057876b16 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamExtractorService.java @@ -0,0 +1,604 @@ +package ca.uhn.fhir.jpa.dao.index; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao; +import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.dao.MatchUrlService; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.UrlUtil; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.*; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.Reference; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; +import java.util.*; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Service +@Lazy +public class SearchParamExtractorService { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamExtractorService.class); + + @Autowired + private DaoConfig myDaoConfig; + @Autowired + private FhirContext myContext; + @Autowired + private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; + @Autowired + private ISearchParamExtractor mySearchParamExtractor; + @Autowired + private ISearchParamRegistry mySearchParamRegistry; + @Autowired + private IdHelperService myIdHelperService; + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired + private MatchUrlService myMatchUrlService; + + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + protected EntityManager myEntityManager; + + + public void populateFromResource(ResourceIndexedSearchParams theParams, IDao theCallingDao, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams existingParams) { + extractFromResource(theParams, theEntity, theResource); + + Set> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet(); + if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) { + theParams.findMissingSearchParams(myDaoConfig, theEntity, activeSearchParams); + } + + theParams.setUpdatedTime(theUpdateTime); + + extractInlineReferences(theResource); + + extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, true); + + /* + * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them + */ + for (Iterator existingLinkIter = existingParams.getResourceLinks().iterator(); existingLinkIter.hasNext(); ) { + ResourceLink nextExisting = existingLinkIter.next(); + if (theParams.links.remove(nextExisting)) { + existingLinkIter.remove(); + theParams.links.add(nextExisting); + } + } + + /* + * Handle composites + */ + extractCompositeStringUniques(theEntity, theParams); + } + + public void extractFromResource(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource) { + theParams.stringParams.addAll(extractSearchParamStrings(theEntity, theResource)); + theParams.numberParams.addAll(extractSearchParamNumber(theEntity, theResource)); + theParams.quantityParams.addAll(extractSearchParamQuantity(theEntity, theResource)); + theParams.dateParams.addAll(extractSearchParamDates(theEntity, theResource)); + theParams.uriParams.addAll(extractSearchParamUri(theEntity, theResource)); + theParams.coordsParams.addAll(extractSearchParamCoords(theEntity, theResource)); + + ourLog.trace("Storing date indexes: {}", theParams.dateParams); + + for (BaseResourceIndexedSearchParam next : extractSearchParamTokens(theEntity, theResource)) { + if (next instanceof ResourceIndexedSearchParamToken) { + theParams.tokenParams.add((ResourceIndexedSearchParamToken) next); + } else { + theParams.stringParams.add((ResourceIndexedSearchParamString) next); + } + } + } + + /** + * Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the + * matching resource. + */ + + public void extractInlineReferences(IBaseResource theResource) { + if (!myDaoConfig.isAllowInlineMatchUrlReferences()) { + return; + } + FhirTerser terser = myContext.newTerser(); + List allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class); + for (IBaseReference nextRef : allRefs) { + IIdType nextId = nextRef.getReferenceElement(); + String nextIdText = nextId.getValue(); + if (nextIdText == null) { + continue; + } + int qmIndex = nextIdText.indexOf('?'); + if (qmIndex != -1) { + for (int i = qmIndex - 1; i >= 0; i--) { + if (nextIdText.charAt(i) == '/') { + if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') { + // Just in case the URL is in the form Patient/?foo=bar + continue; + } + nextIdText = nextIdText.substring(i + 1); + break; + } + } + String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", ""); + RuntimeResourceDefinition matchResourceDef = myContext.getResourceDefinition(resourceTypeString); + if (matchResourceDef == null) { + String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString); + throw new InvalidRequestException(msg); + } + Class matchResourceType = matchResourceDef.getImplementingClass(); + Set matches = myMatchUrlService.processMatchUrl(nextIdText, matchResourceType); + if (matches.isEmpty()) { + String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue()); + throw new ResourceNotFoundException(msg); + } + if (matches.size() > 1) { + String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue()); + throw new PreconditionFailedException(msg); + } + Long next = matches.iterator().next(); + String newId = myIdHelperService.translatePidIdToForcedId(resourceTypeString, next); + ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId); + nextRef.setReference(newId); + } + } + } + + + protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource); + } + + protected Set extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamDates(theEntity, theResource); + } + + protected Set extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamNumber(theEntity, theResource); + } + + protected Set extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamQuantity(theEntity, theResource); + } + + protected Set extractSearchParamStrings(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamStrings(theEntity, theResource); + } + + protected Set extractSearchParamTokens(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamTokens(theEntity, theResource); + } + + protected Set extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) { + return mySearchParamExtractor.extractSearchParamUri(theEntity, theResource); + } + + private void extractCompositeStringUniques(ResourceTable theEntity, ResourceIndexedSearchParams theParams) { + + String resourceType = theEntity.getResourceType(); + List uniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams(resourceType); + + for (JpaRuntimeSearchParam next : uniqueSearchParams) { + + List> partsChoices = new ArrayList<>(); + + for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) { + Collection paramsListForCompositePart = null; + Collection linksForCompositePart = null; + Collection linksForCompositePartWantPaths = null; + switch (nextCompositeOf.getParamType()) { + case NUMBER: + paramsListForCompositePart = theParams.numberParams; + break; + case DATE: + paramsListForCompositePart = theParams.dateParams; + break; + case STRING: + paramsListForCompositePart = theParams.stringParams; + break; + case TOKEN: + paramsListForCompositePart = theParams.tokenParams; + break; + case REFERENCE: + linksForCompositePart = theParams.links; + linksForCompositePartWantPaths = new HashSet<>(); + linksForCompositePartWantPaths.addAll(nextCompositeOf.getPathsSplit()); + break; + case QUANTITY: + paramsListForCompositePart = theParams.quantityParams; + break; + case URI: + paramsListForCompositePart = theParams.uriParams; + break; + case COMPOSITE: + case HAS: + break; + } + + ArrayList nextChoicesList = new ArrayList<>(); + partsChoices.add(nextChoicesList); + + String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName()); + if (paramsListForCompositePart != null) { + for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) { + if (nextParam.getParamName().equals(nextCompositeOf.getName())) { + IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType(); + String value = nextParamAsClientParam.getValueAsQueryToken(myContext); + if (isNotBlank(value)) { + value = UrlUtil.escapeUrlParam(value); + nextChoicesList.add(key + "=" + value); + } + } + } + } + if (linksForCompositePart != null) { + for (ResourceLink nextLink : linksForCompositePart) { + if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) { + String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue(); + if (isNotBlank(value)) { + value = UrlUtil.escapeUrlParam(value); + nextChoicesList.add(key + "=" + value); + } + } + } + } + } + + Set queryStringsToPopulate = theParams.extractCompositeStringUniquesValueChains(resourceType, partsChoices); + + for (String nextQueryString : queryStringsToPopulate) { + if (isNotBlank(nextQueryString)) { + theParams.compositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString)); + } + } + } + } + + @SuppressWarnings("unchecked") + public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, boolean lookUpReferencesInDatabase) { + String resourceType = theEntity.getResourceType(); + + /* + * For now we don't try to load any of the links in a bundle if it's the actual bundle we're storing.. + */ + if (theResource instanceof IBaseBundle) { + return; + } + + Map searchParams = mySearchParamRegistry.getActiveSearchParams(toResourceName(theResource.getClass())); + for (RuntimeSearchParam nextSpDef : searchParams.values()) { + + if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { + continue; + } + + String nextPathsUnsplit = nextSpDef.getPath(); + if (isBlank(nextPathsUnsplit)) { + continue; + } + + boolean multiType = false; + if (nextPathsUnsplit.endsWith("[x]")) { + multiType = true; + } + + List refs = mySearchParamExtractor.extractResourceLinks(theResource, nextSpDef); + for (PathAndRef nextPathAndRef : refs) { + Object nextObject = nextPathAndRef.getRef(); + + /* + * A search parameter on an extension field that contains + * references should index those references + */ + if (nextObject instanceof IBaseExtension) { + nextObject = ((IBaseExtension) nextObject).getValue(); + } + + if (nextObject instanceof CanonicalType) { + nextObject = new Reference(((CanonicalType) nextObject).getValueAsString()); + } + + IIdType nextId; + if (nextObject instanceof IBaseReference) { + IBaseReference nextValue = (IBaseReference) nextObject; + if (nextValue.isEmpty()) { + continue; + } + nextId = nextValue.getReferenceElement(); + + /* + * This can only really happen if the DAO is being called + * programatically with a Bundle (not through the FHIR REST API) + * but Smile does this + */ + if (nextId.isEmpty() && nextValue.getResource() != null) { + nextId = nextValue.getResource().getIdElement(); + } + + if (nextId.isEmpty() || nextId.getValue().startsWith("#")) { + // This is a blank or contained resource reference + continue; + } + } else if (nextObject instanceof IBaseResource) { + nextId = ((IBaseResource) nextObject).getIdElement(); + if (nextId == null || nextId.hasIdPart() == false) { + continue; + } + } else if (myContext.getElementDefinition((Class) nextObject.getClass()).getName().equals("uri")) { + continue; + } else if (resourceType.equals("Consent") && nextPathAndRef.getPath().equals("Consent.source")) { + // Consent#source-identifier has a path that isn't typed - This is a one-off to deal with that + continue; + } else { + if (!multiType) { + if (nextSpDef.getName().equals("sourceuri")) { + continue; // TODO: disable this eventually - ConceptMap:sourceuri is of type reference but points to a URI + } + throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass()); + } else { + continue; + } + } + + theParams.populatedResourceLinkParameters.add(nextSpDef.getName()); + + if (LogicalReferenceHelper.isLogicalReference(myDaoConfig, nextId)) { + ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime); + if (theParams.links.add(resourceLink)) { + ourLog.debug("Indexing remote resource reference URL: {}", nextId); + } + continue; + } + + String baseUrl = nextId.getBaseUrl(); + String typeString = nextId.getResourceType(); + if (isBlank(typeString)) { + throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue()); + } + RuntimeResourceDefinition resourceDefinition; + try { + resourceDefinition = myContext.getResourceDefinition(typeString); + } catch (DataFormatException e) { + throw new InvalidRequestException( + "Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue()); + } + + if (isNotBlank(baseUrl)) { + if (!myDaoConfig.getTreatBaseUrlsAsLocal().contains(baseUrl) && !myDaoConfig.isAllowExternalReferences()) { + String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "externalReferenceNotAllowed", nextId.getValue()); + throw new InvalidRequestException(msg); + } else { + ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime); + if (theParams.links.add(resourceLink)) { + ourLog.debug("Indexing remote resource reference URL: {}", nextId); + } + continue; + } + } + + Class type = resourceDefinition.getImplementingClass(); + String id = nextId.getIdPart(); + if (StringUtils.isBlank(id)) { + throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue()); + } + + myDaoRegistry.getDaoOrThrowException(type); + ResourceTable target; + if (lookUpReferencesInDatabase) { + Long valueOf; + try { + valueOf = myIdHelperService.translateForcedIdToPid(typeString, id); + } catch (ResourceNotFoundException e) { + if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) { + continue; + } + RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(type); + String resName = missingResourceDef.getName(); + + if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) { + IBaseResource newResource = missingResourceDef.newInstance(); + newResource.setId(resName + "/" + id); + IFhirResourceDao placeholderResourceDao = (IFhirResourceDao) myDaoRegistry.getResourceDao(newResource.getClass()); + ourLog.debug("Automatically creating empty placeholder resource: {}", newResource.getIdElement().getValue()); + valueOf = placeholderResourceDao.update(newResource).getEntity().getId(); + } else { + throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit); + } + } + target = myEntityManager.find(ResourceTable.class, valueOf); + RuntimeResourceDefinition targetResourceDef = myContext.getResourceDefinition(type); + if (target == null) { + String resName = targetResourceDef.getName(); + throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit); + } + + if (!typeString.equals(target.getResourceType())) { + throw new UnprocessableEntityException( + "Resource contains reference to " + nextId.getValue() + " but resource with ID " + nextId.getIdPart() + " is actually of type " + target.getResourceType()); + } + + if (target.getDeleted() != null) { + String resName = targetResourceDef.getName(); + throw new InvalidRequestException("Resource " + resName + "/" + id + " is deleted, specified in path: " + nextPathsUnsplit); + } + + if (nextSpDef.getTargets() != null && !nextSpDef.getTargets().contains(typeString)) { + continue; + } + } else { + target = new ResourceTable(); + target.setResourceType(typeString); + if (nextId.isIdPartValidLong()) { + target.setId(nextId.getIdPartAsLong()); + } else { + ForcedId forcedId = new ForcedId(); + forcedId.setForcedId(id); + target.setForcedId(forcedId); + } + } + ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, target, theUpdateTime); + theParams.links.add(resourceLink); + } + + } + + theEntity.setHasLinks(theParams.links.size() > 0); + } + + public String toResourceName(Class theResourceType) { + return myContext.getResourceDefinition(theResourceType).getName(); + } + + public void removeCommon(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) { + theParams.calculateHashes(theParams.stringParams); + for (ResourceIndexedSearchParamString next : removeCommon(existingParams.stringParams, theParams.stringParams)) { + next.setDaoConfig(myDaoConfig); + myEntityManager.remove(next); + theEntity.getParamsString().remove(next); + } + for (ResourceIndexedSearchParamString next : removeCommon(theParams.stringParams, existingParams.stringParams)) { + myEntityManager.persist(next); + } + + theParams.calculateHashes(theParams.tokenParams); + for (ResourceIndexedSearchParamToken next : removeCommon(existingParams.tokenParams, theParams.tokenParams)) { + myEntityManager.remove(next); + theEntity.getParamsToken().remove(next); + } + for (ResourceIndexedSearchParamToken next : removeCommon(theParams.tokenParams, existingParams.tokenParams)) { + myEntityManager.persist(next); + } + + theParams.calculateHashes(theParams.numberParams); + for (ResourceIndexedSearchParamNumber next : removeCommon(existingParams.numberParams, theParams.numberParams)) { + myEntityManager.remove(next); + theEntity.getParamsNumber().remove(next); + } + for (ResourceIndexedSearchParamNumber next : removeCommon(theParams.numberParams, existingParams.numberParams)) { + myEntityManager.persist(next); + } + + theParams.calculateHashes(theParams.quantityParams); + for (ResourceIndexedSearchParamQuantity next : removeCommon(existingParams.quantityParams, theParams.quantityParams)) { + myEntityManager.remove(next); + theEntity.getParamsQuantity().remove(next); + } + for (ResourceIndexedSearchParamQuantity next : removeCommon(theParams.quantityParams, existingParams.quantityParams)) { + myEntityManager.persist(next); + } + + // Store date SP's + theParams.calculateHashes(theParams.dateParams); + for (ResourceIndexedSearchParamDate next : removeCommon(existingParams.dateParams, theParams.dateParams)) { + myEntityManager.remove(next); + theEntity.getParamsDate().remove(next); + } + for (ResourceIndexedSearchParamDate next : removeCommon(theParams.dateParams, existingParams.dateParams)) { + myEntityManager.persist(next); + } + + // Store URI SP's + theParams.calculateHashes(theParams.uriParams); + for (ResourceIndexedSearchParamUri next : removeCommon(existingParams.uriParams, theParams.uriParams)) { + myEntityManager.remove(next); + theEntity.getParamsUri().remove(next); + } + for (ResourceIndexedSearchParamUri next : removeCommon(theParams.uriParams, existingParams.uriParams)) { + myEntityManager.persist(next); + } + + // Store Coords SP's + theParams.calculateHashes(theParams.coordsParams); + for (ResourceIndexedSearchParamCoords next : removeCommon(existingParams.coordsParams, theParams.coordsParams)) { + myEntityManager.remove(next); + theEntity.getParamsCoords().remove(next); + } + for (ResourceIndexedSearchParamCoords next : removeCommon(theParams.coordsParams, existingParams.coordsParams)) { + myEntityManager.persist(next); + } + + // Store resource links + for (ResourceLink next : removeCommon(existingParams.links, theParams.links)) { + myEntityManager.remove(next); + theEntity.getResourceLinks().remove(next); + } + for (ResourceLink next : removeCommon(theParams.links, existingParams.links)) { + myEntityManager.persist(next); + } + + // make sure links are indexed + theEntity.setResourceLinks(theParams.links); + + // Store composite string uniques + if (myDaoConfig.isUniqueIndexesEnabled()) { + for (ResourceIndexedCompositeStringUnique next : removeCommon(existingParams.compositeStringUniques, theParams.compositeStringUniques)) { + ourLog.debug("Removing unique index: {}", next); + myEntityManager.remove(next); + theEntity.getParamsCompositeStringUnique().remove(next); + } + for (ResourceIndexedCompositeStringUnique next : removeCommon(theParams.compositeStringUniques, existingParams.compositeStringUniques)) { + if (myDaoConfig.isUniqueIndexesCheckedBeforeSave()) { + ResourceIndexedCompositeStringUnique existing = myResourceIndexedCompositeStringUniqueDao.findByQueryString(next.getIndexString()); + if (existing != null) { + String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue()); + throw new PreconditionFailedException(msg); + } + } + ourLog.debug("Persisting unique index: {}", next); + myEntityManager.persist(next); + } + } + } + + private Collection removeCommon(Collection theInput, Collection theToRemove) { + assert theInput != theToRemove; + + if (theInput.isEmpty()) { + return theInput; + } + + ArrayList retVal = new ArrayList<>(theInput); + retVal.removeAll(theToRemove); + return retVal; + } +} + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCodeSystemR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCodeSystemR4.java index d3a99267ead..9547d122408 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCodeSystemR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCodeSystemR4.java @@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystem; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; -import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.jpa.util.LogicUtil; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenParam; @@ -64,8 +63,6 @@ public class FhirResourceDaoCodeSystemR4 extends FhirResourceDaoR4 i @Autowired private ITermCodeSystemDao myCsDao; @Autowired - private IHapiTerminologySvc myTerminologySvc; - @Autowired private ValidationSupportChain myValidationSupport; @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java index 5f54f36c521..c6fe489bf5f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java @@ -156,4 +156,7 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { return hashCode.asLong(); } + public boolean matches(IQueryParameterType theParam) { + throw new UnsupportedOperationException("No parameter matcher for "+theParam); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java index 7b43ebf9dce..b41a8c9b0a3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -184,4 +185,31 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar b.append("valueHigh", new InstantDt(getValueHigh())); return b.build(); } + + @Override + public boolean matches(IQueryParameterType theParam) { + if (!(theParam instanceof DateParam)) { + return false; + } + DateParam date = (DateParam) theParam; + DateRangeParam range = new DateRangeParam(date); + Date lowerBound = range.getLowerBoundAsInstant(); + Date upperBound = range.getUpperBoundAsInstant(); + + if (lowerBound == null && upperBound == null) { + // should never happen + return false; + } + + boolean result = true; + if (lowerBound != null) { + result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound)); + result &= (myValueHigh.after(lowerBound) || myValueHigh.equals(lowerBound)); + } + if (upperBound != null) { + result &= (myValueLow.before(upperBound) || myValueLow.equals(upperBound)); + result &= (myValueHigh.before(upperBound) || myValueHigh.equals(upperBound)); + } + return result; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java index ed5568fbd92..27dff6c5f7f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java @@ -149,4 +149,13 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP b.append("value", getValue()); return b.build(); } + + @Override + public boolean matches(IQueryParameterType theParam) { + if (!(theParam instanceof NumberParam)) { + return false; + } + NumberParam number = (NumberParam)theParam; + return getValue().equals(number.getValue()); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java index 13c7e7fc2ad..8afa70e6ae4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java @@ -235,4 +235,37 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc return hash(theResourceType, theParamName, theUnits); } + @Override + public boolean matches(IQueryParameterType theParam) { + if (!(theParam instanceof QuantityParam)) { + return false; + } + QuantityParam quantity = (QuantityParam)theParam; + boolean retval = false; + + // Only match on system if it wasn't specified + if (quantity.getSystem() == null && quantity.getUnits() == null) { + if (getValue().equals(quantity.getValue())) { + retval = true; + } + } else if (quantity.getSystem() == null) { + if (getUnits().equalsIgnoreCase(quantity.getUnits()) && + getValue().equals(quantity.getValue())) { + retval = true; + } + } else if (quantity.getUnits() == null) { + if (getSystem().equalsIgnoreCase(quantity.getSystem()) && + getValue().equals(quantity.getValue())) { + retval = true; + } + } else { + if (getSystem().equalsIgnoreCase(quantity.getSystem()) && + getUnits().equalsIgnoreCase(quantity.getUnits()) && + getValue().equals(quantity.getValue())) { + retval = true; + } + } + return retval; + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java index 023199b395f..6ed9cd47278 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.param.StringParam; @@ -294,4 +295,13 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP return hash(theResourceType, theParamName, left(theValueNormalized, hashPrefixLength)); } + @Override + public boolean matches(IQueryParameterType theParam) { + if (!(theParam instanceof StringParam)) { + return false; + } + StringParam string = (StringParam)theParam; + String normalizedString = BaseHapiFhirDao.normalizeString(string.getValue()); + return getValueNormalized().startsWith(normalizedString); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java index 74a86253195..6b628b7e794 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java @@ -251,4 +251,29 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa public static long calculateHashValue(String theResourceType, String theParamName, String theValue) { return hash(theResourceType, theParamName, trim(theValue)); } + + @Override + public boolean matches(IQueryParameterType theParam) { + if (!(theParam instanceof TokenParam)) { + return false; + } + TokenParam token = (TokenParam)theParam; + boolean retval = false; + // Only match on system if it wasn't specified + if (token.getSystem() == null || token.getSystem().isEmpty()) { + if (getValue().equalsIgnoreCase(token.getValue())) { + retval = true; + } + } else if (token.getValue() == null || token.getValue().isEmpty()) { + if (token.getSystem().equalsIgnoreCase(getSystem())) { + retval = true; + } + } else { + if (token.getSystem().equalsIgnoreCase(getSystem()) && + getValue().equalsIgnoreCase(token.getValue())) { + retval = true; + } + } + return retval; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java index b94ee78db6f..0fe11231866 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java @@ -179,4 +179,13 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara return hash(theResourceType, theParamName, theUri); } + @Override + public boolean matches(IQueryParameterType theParam) { + if (!(theParam instanceof UriParam)) { + return false; + } + UriParam uri = (UriParam)theParam; + return getUri().equalsIgnoreCase(uri.getValueNotNull()); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceReindexJobEntity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceReindexJobEntity.java index 9676d2f1aa8..916e61cd33a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceReindexJobEntity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceReindexJobEntity.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.entity; * 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. @@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.entity; */ import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import javax.persistence.*; import java.io.Serializable; @@ -53,6 +55,16 @@ public class ResourceReindexJobEntity implements Serializable { @Column(name = "SUSPENDED_UNTIL", nullable = true) @Temporal(TemporalType.TIMESTAMP) private Date mySuspendedUntil; + @Column(name = "REINDEX_COUNT", nullable = true) + private Integer myReindexCount; + + public Integer getReindexCount() { + return myReindexCount; + } + + public void setReindexCount(Integer theReindexCount) { + myReindexCount = theReindexCount; + } public Date getSuspendedUntil() { return mySuspendedUntil; @@ -110,4 +122,20 @@ public class ResourceReindexJobEntity implements Serializable { public void setDeleted(boolean theDeleted) { myDeleted = theDeleted; } + + @Override + public String toString() { + ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("id", myId) + .append("resourceType", myResourceType) + .append("thresholdLow", myThresholdLow) + .append("thresholdHigh", myThresholdHigh); + if (myDeleted) { + b.append("deleted", myDeleted); + } + if (mySuspendedUntil != null) { + b.append("suspendedUntil", mySuspendedUntil); + } + return b.toString(); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java index e30a9b83438..47dffe14094 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java @@ -87,10 +87,6 @@ public class ResourceTable extends BaseHasResource implements Serializable { @Column(name = "RES_ID") private Long myId; - @OneToMany(mappedBy = "myTargetResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) - @OptimisticLock(excluded = true) - private Collection myIncomingResourceLinks; - @Column(name = "SP_INDEX_STATUS", nullable = true) @OptimisticLock(excluded = true) private Long myIndexStatus; @@ -181,31 +177,36 @@ public class ResourceTable extends BaseHasResource implements Serializable { @OptimisticLock(excluded = true) private Collection myParamsCompositeStringUnique; + @IndexedEmbedded @OneToMany(mappedBy = "mySourceResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) - @IndexedEmbedded() @OptimisticLock(excluded = true) private Collection myResourceLinks; - + @OneToMany(mappedBy = "myTargetResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) + private Collection myResourceLinksAsTarget; @Column(name = "RES_TYPE", length = RESTYPE_LEN) @Field @OptimisticLock(excluded = true) private String myResourceType; - @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) @OptimisticLock(excluded = true) private Collection mySearchParamPresents; - @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) @OptimisticLock(excluded = true) private Set myTags; - @Transient private transient boolean myUnchangedInCurrentOperation; - @Version @Column(name = "RES_VER") private long myVersion; + public Collection getResourceLinksAsTarget() { + if (myResourceLinksAsTarget == null) { + myResourceLinksAsTarget = new ArrayList<>(); + } + return myResourceLinksAsTarget; + } + @Override public ResourceTag addTag(TagDefinition theTag) { for (ResourceTag next : getTags()) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java index bdbebf91a69..f2bef04364e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/graphql/JpaStorageServices.java @@ -64,7 +64,7 @@ public class JpaStorageServices extends BaseHapiFhirDao implement for (Argument nextArgument : theSearchParams) { - RuntimeSearchParam searchParam = getSearchParamByName(typeDef, nextArgument.getName()); + RuntimeSearchParam searchParam = mySearchParamRegistry.getSearchParamByName(typeDef, nextArgument.getName()); for (Value nextValue : nextArgument.getValues()) { String value = nextValue.getValue(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java index 2386ea4070a..8b13296dd4f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/SubscriptionTriggeringProvider.java @@ -21,133 +21,36 @@ package ca.uhn.fhir.jpa.provider; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; -import ca.uhn.fhir.jpa.dao.DaoRegistry; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl; -import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; -import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; +import ca.uhn.fhir.jpa.subscription.ISubscriptionTriggeringSvc; import ca.uhn.fhir.jpa.util.JpaConstants; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; -import ca.uhn.fhir.rest.api.CacheControlDirective; -import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; -import ca.uhn.fhir.util.ParametersUtil; -import ca.uhn.fhir.util.StopWatch; -import ca.uhn.fhir.util.ValidateUtil; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.Validate; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import org.apache.commons.lang3.time.DateUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.hl7.fhir.instance.model.IdType; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.scheduling.annotation.Scheduled; -import javax.annotation.PostConstruct; -import java.util.*; -import java.util.concurrent.*; -import java.util.stream.Collectors; - -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -public class SubscriptionTriggeringProvider implements IResourceProvider, ApplicationContextAware { +import java.util.List; +public class SubscriptionTriggeringProvider implements IResourceProvider { public static final String RESOURCE_ID = "resourceId"; - public static final int DEFAULT_MAX_SUBMIT = 10000; public static final String SEARCH_URL = "searchUrl"; - private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTriggeringProvider.class); - private final List myActiveJobs = new ArrayList<>(); @Autowired private FhirContext myFhirContext; @Autowired - private DaoRegistry myDaoRegistry; - private List> mySubscriptionInterceptorList; - private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT; - @Autowired - private ISearchCoordinatorSvc mySearchCoordinatorSvc; - private ApplicationContext myAppCtx; - private ExecutorService myExecutorService; + private ISubscriptionTriggeringSvc mySubscriptionTriggeringSvc; - /** - * Sets the maximum number of resources that will be submitted in a single pass - */ - public void setMaxSubmitPerPass(Integer theMaxSubmitPerPass) { - Integer maxSubmitPerPass = theMaxSubmitPerPass; - if (maxSubmitPerPass == null) { - maxSubmitPerPass = DEFAULT_MAX_SUBMIT; - } - Validate.isTrue(maxSubmitPerPass > 0, "theMaxSubmitPerPass must be > 0"); - myMaxSubmitPerPass = maxSubmitPerPass; - } - - @SuppressWarnings("unchecked") - @PostConstruct - public void start() { - mySubscriptionInterceptorList = ObjectUtils.defaultIfNull(mySubscriptionInterceptorList, Collections.emptyList()); - mySubscriptionInterceptorList = new ArrayList<>(); - Collection values1 = myAppCtx.getBeansOfType(BaseSubscriptionInterceptor.class).values(); - Collection> values = (Collection>) values1; - mySubscriptionInterceptorList.addAll(values); - - - LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(1000); - BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() - .namingPattern("SubscriptionTriggering-%d") - .daemon(false) - .priority(Thread.NORM_PRIORITY) - .build(); - RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() { - @Override - public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) { - ourLog.info("Note: Subscription triggering queue is full ({} elements), waiting for a slot to become available!", executorQueue.size()); - StopWatch sw = new StopWatch(); - try { - executorQueue.put(theRunnable); - } catch (InterruptedException theE) { - throw new RejectedExecutionException("Task " + theRunnable.toString() + - " rejected from " + theE.toString()); - } - ourLog.info("Slot become available after {}ms", sw.getMillis()); - } - }; - myExecutorService = new ThreadPoolExecutor( - 0, - 10, - 0L, - TimeUnit.MILLISECONDS, - executorQueue, - threadFactory, - rejectedExecutionHandler); - - } @Operation(name = JpaConstants.OPERATION_TRIGGER_SUBSCRIPTION) public IBaseParameters triggerSubscription( @OperationParam(name = RESOURCE_ID, min = 0, max = OperationParam.MAX_UNLIMITED) List theResourceIds, @OperationParam(name = SEARCH_URL, min = 0, max = OperationParam.MAX_UNLIMITED) List theSearchUrls ) { - return doTriggerSubscription(theResourceIds, theSearchUrls, null); + return mySubscriptionTriggeringSvc.triggerSubscription(theResourceIds, theSearchUrls, null); } @Operation(name = JpaConstants.OPERATION_TRIGGER_SUBSCRIPTION) @@ -156,331 +59,13 @@ public class SubscriptionTriggeringProvider implements IResourceProvider, Applic @OperationParam(name = RESOURCE_ID, min = 0, max = OperationParam.MAX_UNLIMITED) List theResourceIds, @OperationParam(name = SEARCH_URL, min = 0, max = OperationParam.MAX_UNLIMITED) List theSearchUrls ) { - - // Throw a 404 if the subscription doesn't exist - IFhirResourceDao subscriptionDao = myDaoRegistry.getResourceDao("Subscription"); - IIdType subscriptionId = theSubscriptionId; - if (subscriptionId.hasResourceType() == false) { - subscriptionId = subscriptionId.withResourceType("Subscription"); - } - subscriptionDao.read(subscriptionId); - - return doTriggerSubscription(theResourceIds, theSearchUrls, subscriptionId); - + return mySubscriptionTriggeringSvc.triggerSubscription(theResourceIds, theSearchUrls, theSubscriptionId); } - private IBaseParameters doTriggerSubscription(@OperationParam(name = RESOURCE_ID, min = 0, max = OperationParam.MAX_UNLIMITED) List theResourceIds, @OperationParam(name = SEARCH_URL, min = 0, max = OperationParam.MAX_UNLIMITED) List theSearchUrls, @IdParam IIdType theSubscriptionId) { - if (mySubscriptionInterceptorList.isEmpty()) { - throw new PreconditionFailedException("Subscription processing not active on this server"); - } - - List resourceIds = ObjectUtils.defaultIfNull(theResourceIds, Collections.emptyList()); - List searchUrls = ObjectUtils.defaultIfNull(theSearchUrls, Collections.emptyList()); - - // Make sure we have at least one resource ID or search URL - if (resourceIds.size() == 0 && searchUrls.size() == 0) { - throw new InvalidRequestException("No resource IDs or search URLs specified for triggering"); - } - - // Resource URLs must be compete - for (UriParam next : resourceIds) { - IdType resourceId = new IdType(next.getValue()); - ValidateUtil.isTrueOrThrowInvalidRequest(resourceId.hasResourceType(), RESOURCE_ID + " parameter must have resource type"); - ValidateUtil.isTrueOrThrowInvalidRequest(resourceId.hasIdPart(), RESOURCE_ID + " parameter must have resource ID part"); - } - - // Search URLs must be valid - for (StringParam next : searchUrls) { - if (next.getValue().contains("?") == false) { - throw new InvalidRequestException("Search URL is not valid (must be in the form \"[resource type]?[optional params]\")"); - } - } - - SubscriptionTriggeringJobDetails jobDetails = new SubscriptionTriggeringJobDetails(); - jobDetails.setJobId(UUID.randomUUID().toString()); - jobDetails.setRemainingResourceIds(resourceIds.stream().map(UriParam::getValue).collect(Collectors.toList())); - jobDetails.setRemainingSearchUrls(searchUrls.stream().map(StringParam::getValue).collect(Collectors.toList())); - if (theSubscriptionId != null) { - jobDetails.setSubscriptionId(theSubscriptionId.toUnqualifiedVersionless().getValue()); - } - - // Submit job for processing - synchronized (myActiveJobs) { - myActiveJobs.add(jobDetails); - } - ourLog.info("Subscription triggering requested for {} resource and {} search - Gave job ID: {}", resourceIds.size(), searchUrls.size(), jobDetails.getJobId()); - - // Create a parameters response - IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext); - IPrimitiveType value = (IPrimitiveType) myFhirContext.getElementDefinition("string").newInstance(); - value.setValueAsString("Subscription triggering job submitted as JOB ID: " + jobDetails.myJobId); - ParametersUtil.addParameterToParameters(myFhirContext, retVal, "information", value); - return retVal; - } @Override public Class getResourceType() { return myFhirContext.getResourceDefinition("Subscription").getImplementingClass(); } - @Scheduled(fixedDelay = DateUtils.MILLIS_PER_SECOND) - public void runDeliveryPass() { - - synchronized (myActiveJobs) { - if (myActiveJobs.isEmpty()) { - return; - } - - String activeJobIds = myActiveJobs.stream().map(t->t.getJobId()).collect(Collectors.joining(", ")); - ourLog.info("Starting pass: currently have {} active job IDs: {}", myActiveJobs.size(), activeJobIds); - - SubscriptionTriggeringJobDetails activeJob = myActiveJobs.get(0); - - runJob(activeJob); - - // If the job is complete, remove it from the queue - if (activeJob.getRemainingResourceIds().isEmpty()) { - if (activeJob.getRemainingSearchUrls().isEmpty()) { - if (isBlank(activeJob.myCurrentSearchUuid)) { - myActiveJobs.remove(0); - String remainingJobsMsg = ""; - if (myActiveJobs.size() > 0) { - remainingJobsMsg = "(" + myActiveJobs.size() + " jobs remaining)"; - } - ourLog.info("Subscription triggering job {} is complete{}", activeJob.getJobId(), remainingJobsMsg); - } - } - } - - } - - } - - private void runJob(SubscriptionTriggeringJobDetails theJobDetails) { - StopWatch sw = new StopWatch(); - ourLog.info("Starting pass of subscription triggering job {}", theJobDetails.getJobId()); - - // Submit individual resources - int totalSubmitted = 0; - List>> futures = new ArrayList<>(); - while (theJobDetails.getRemainingResourceIds().size() > 0 && totalSubmitted < myMaxSubmitPerPass) { - totalSubmitted++; - String nextResourceId = theJobDetails.getRemainingResourceIds().remove(0); - Future future = submitResource(theJobDetails.getSubscriptionId(), nextResourceId); - futures.add(Pair.of(nextResourceId, future)); - } - - // Make sure these all succeeded in submitting - if (validateFuturesAndReturnTrueIfWeShouldAbort(futures)) { - return; - } - - // If we don't have an active search started, and one needs to be.. start it - if (isBlank(theJobDetails.getCurrentSearchUuid()) && theJobDetails.getRemainingSearchUrls().size() > 0 && totalSubmitted < myMaxSubmitPerPass) { - String nextSearchUrl = theJobDetails.getRemainingSearchUrls().remove(0); - RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myFhirContext, nextSearchUrl); - String queryPart = nextSearchUrl.substring(nextSearchUrl.indexOf('?')); - String resourceType = resourceDef.getName(); - - IFhirResourceDao callingDao = myDaoRegistry.getResourceDao(resourceType); - SearchParameterMap params = BaseHapiFhirDao.translateMatchUrl(callingDao, myFhirContext, queryPart, resourceDef); - - ourLog.info("Triggering job[{}] is starting a search for {}", theJobDetails.getJobId(), nextSearchUrl); - - IBundleProvider search = mySearchCoordinatorSvc.registerSearch(callingDao, params, resourceType, new CacheControlDirective()); - theJobDetails.setCurrentSearchUuid(search.getUuid()); - theJobDetails.setCurrentSearchResourceType(resourceType); - theJobDetails.setCurrentSearchCount(params.getCount()); - theJobDetails.setCurrentSearchLastUploadedIndex(-1); - } - - // If we have an active search going, submit resources from it - if (isNotBlank(theJobDetails.getCurrentSearchUuid()) && totalSubmitted < myMaxSubmitPerPass) { - int fromIndex = theJobDetails.getCurrentSearchLastUploadedIndex() + 1; - - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theJobDetails.getCurrentSearchResourceType()); - - int maxQuerySize = myMaxSubmitPerPass - totalSubmitted; - int toIndex = fromIndex + maxQuerySize; - if (theJobDetails.getCurrentSearchCount() != null) { - toIndex = Math.min(toIndex, theJobDetails.getCurrentSearchCount()); - } - ourLog.info("Triggering job[{}] search {} requesting resources {} - {}", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex); - List resourceIds = mySearchCoordinatorSvc.getResources(theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex); - - ourLog.info("Triggering job[{}] delivering {} resources", theJobDetails.getJobId(), resourceIds.size()); - int highestIndexSubmitted = theJobDetails.getCurrentSearchLastUploadedIndex(); - - for (Long next : resourceIds) { - IBaseResource nextResource = resourceDao.readByPid(next); - Future future = submitResource(theJobDetails.getSubscriptionId(), nextResource); - futures.add(Pair.of(nextResource.getIdElement().getIdPart(), future)); - totalSubmitted++; - highestIndexSubmitted++; - } - - if (validateFuturesAndReturnTrueIfWeShouldAbort(futures)) { - return; - } - - theJobDetails.setCurrentSearchLastUploadedIndex(highestIndexSubmitted); - - if (resourceIds.size() == 0 || (theJobDetails.getCurrentSearchCount() != null && toIndex >= theJobDetails.getCurrentSearchCount())) { - ourLog.info("Triggering job[{}] search {} has completed ", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid()); - theJobDetails.setCurrentSearchResourceType(null); - theJobDetails.setCurrentSearchUuid(null); - theJobDetails.setCurrentSearchLastUploadedIndex(-1); - theJobDetails.setCurrentSearchCount(null); - } - } - - ourLog.info("Subscription trigger job[{}] triggered {} resources in {}ms ({} res / second)", theJobDetails.getJobId(), totalSubmitted, sw.getMillis(), sw.getThroughput(totalSubmitted, TimeUnit.SECONDS)); - } - - private boolean validateFuturesAndReturnTrueIfWeShouldAbort(List>> theIdToFutures) { - - for (Pair> next : theIdToFutures) { - String nextDeliveredId = next.getKey(); - try { - Future nextFuture = next.getValue(); - nextFuture.get(); - ourLog.info("Finished redelivering {}", nextDeliveredId); - } catch (Exception e) { - ourLog.error("Failure triggering resource " + nextDeliveredId, e); - return true; - } - } - - // Clear the list since it will potentially get reused - theIdToFutures.clear(); - return false; - } - - private Future submitResource(String theSubscriptionId, String theResourceIdToTrigger) { - org.hl7.fhir.r4.model.IdType resourceId = new org.hl7.fhir.r4.model.IdType(theResourceIdToTrigger); - IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceId.getResourceType()); - IBaseResource resourceToTrigger = dao.read(resourceId); - - return submitResource(theSubscriptionId, resourceToTrigger); - } - - private Future submitResource(String theSubscriptionId, IBaseResource theResourceToTrigger) { - - ourLog.info("Submitting resource {} to subscription {}", theResourceToTrigger.getIdElement().toUnqualifiedVersionless().getValue(), theSubscriptionId); - - ResourceModifiedMessage msg = new ResourceModifiedMessage(); - msg.setId(theResourceToTrigger.getIdElement()); - msg.setOperationType(ResourceModifiedMessage.OperationTypeEnum.UPDATE); - msg.setSubscriptionId(new IdType(theSubscriptionId).toUnqualifiedVersionless().getValue()); - msg.setNewPayload(myFhirContext, theResourceToTrigger); - - return myExecutorService.submit(()->{ - for (int i = 0; ; i++) { - try { - for (BaseSubscriptionInterceptor next : mySubscriptionInterceptorList) { - next.submitResourceModified(msg); - } - break; - } catch (Exception e) { - if (i >= 3) { - throw new InternalErrorException(e); - } - - ourLog.warn("Exception while retriggering subscriptions (going to sleep and retry): {}", e.toString()); - Thread.sleep(1000); - } - } - - return null; - }); - - } - - public void cancelAll() { - synchronized (myActiveJobs) { - myActiveJobs.clear(); - } - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - myAppCtx = applicationContext; - } - - private static class SubscriptionTriggeringJobDetails { - - private String myJobId; - private String mySubscriptionId; - private List myRemainingResourceIds; - private List myRemainingSearchUrls; - private String myCurrentSearchUuid; - private Integer myCurrentSearchCount; - private String myCurrentSearchResourceType; - private int myCurrentSearchLastUploadedIndex; - - public Integer getCurrentSearchCount() { - return myCurrentSearchCount; - } - - public void setCurrentSearchCount(Integer theCurrentSearchCount) { - myCurrentSearchCount = theCurrentSearchCount; - } - - public String getCurrentSearchResourceType() { - return myCurrentSearchResourceType; - } - - public void setCurrentSearchResourceType(String theCurrentSearchResourceType) { - myCurrentSearchResourceType = theCurrentSearchResourceType; - } - - public String getJobId() { - return myJobId; - } - - public void setJobId(String theJobId) { - myJobId = theJobId; - } - - public String getSubscriptionId() { - return mySubscriptionId; - } - - public void setSubscriptionId(String theSubscriptionId) { - mySubscriptionId = theSubscriptionId; - } - - public List getRemainingResourceIds() { - return myRemainingResourceIds; - } - - public void setRemainingResourceIds(List theRemainingResourceIds) { - myRemainingResourceIds = theRemainingResourceIds; - } - - public List getRemainingSearchUrls() { - return myRemainingSearchUrls; - } - - public void setRemainingSearchUrls(List theRemainingSearchUrls) { - myRemainingSearchUrls = theRemainingSearchUrls; - } - - public String getCurrentSearchUuid() { - return myCurrentSearchUuid; - } - - public void setCurrentSearchUuid(String theCurrentSearchUuid) { - myCurrentSearchUuid = theCurrentSearchUuid; - } - - public int getCurrentSearchLastUploadedIndex() { - return myCurrentSearchLastUploadedIndex; - } - - public void setCurrentSearchLastUploadedIndex(int theCurrentSearchLastUploadedIndex) { - myCurrentSearchLastUploadedIndex = theCurrentSearchLastUploadedIndex; - } - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaConformanceProviderDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaConformanceProviderDstu3.java index 8f2962d182e..93bb447d7ab 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaConformanceProviderDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaConformanceProviderDstu3.java @@ -19,21 +19,23 @@ package ca.uhn.fhir.jpa.provider.dstu3; * limitations under the License. * #L% */ -import java.util.*; - -import javax.servlet.http.HttpServletRequest; - -import org.hl7.fhir.dstu3.model.*; -import org.hl7.fhir.dstu3.model.CapabilityStatement.*; -import org.hl7.fhir.dstu3.model.Enumerations.SearchParamType; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.ExtensionConstants; +import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.CapabilityStatement.*; +import org.hl7.fhir.dstu3.model.Enumerations.SearchParamType; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; @@ -41,21 +43,21 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se private volatile CapabilityStatement myCachedValue; private DaoConfig myDaoConfig; + private ISearchParamRegistry mySearchParamRegistry; private String myImplementationDescription; private boolean myIncludeResourceCounts; private RestfulServer myRestfulServer; private IFhirSystemDao mySystemDao; - /** * Constructor */ @CoverageIgnore - public JpaConformanceProviderDstu3(){ + public JpaConformanceProviderDstu3() { super(); super.setCache(false); setIncludeResourceCounts(true); } - + /** * Constructor */ @@ -65,9 +67,14 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se mySystemDao = theSystemDao; myDaoConfig = theDaoConfig; super.setCache(false); + setSearchParamRegistry(theSystemDao.getSearchParamRegistry()); setIncludeResourceCounts(true); } + public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) { + mySearchParamRegistry = theSearchParamRegistry; + } + @Override public CapabilityStatement getServerConformance(HttpServletRequest theRequest) { CapabilityStatement retVal = myCachedValue; @@ -84,7 +91,7 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se for (CapabilityStatementRestResourceComponent nextResource : nextRest.getResource()) { nextResource.setVersioning(ResourceVersionPolicy.VERSIONEDUPDATE); - + ConditionalDeleteStatus conditionalDelete = nextResource.getConditionalDelete(); if (conditionalDelete == ConditionalDeleteStatus.MULTIPLE && myDaoConfig.isAllowMultipleDelete() == false) { nextResource.setConditionalDelete(ConditionalDeleteStatus.SINGLE); @@ -99,7 +106,7 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se nextResource.getSearchParam().clear(); String resourceName = nextResource.getType(); RuntimeResourceDefinition resourceDef = myRestfulServer.getFhirContext().getResourceDefinition(resourceName); - Collection searchParams = mySystemDao.getSearchParamsByResourceType(resourceDef); + Collection searchParams = mySearchParamRegistry.getSearchParamsByResourceType(resourceDef); for (RuntimeSearchParam runtimeSp : searchParams) { CapabilityStatementRestResourceSearchParamComponent confSp = nextResource.addSearchParam(); @@ -107,42 +114,42 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se confSp.setDocumentation(runtimeSp.getDescription()); confSp.setDefinition(runtimeSp.getUri()); switch (runtimeSp.getParamType()) { - case COMPOSITE: - confSp.setType(SearchParamType.COMPOSITE); - break; - case DATE: - confSp.setType(SearchParamType.DATE); - break; - case NUMBER: - confSp.setType(SearchParamType.NUMBER); - break; - case QUANTITY: - confSp.setType(SearchParamType.QUANTITY); - break; - case REFERENCE: - confSp.setType(SearchParamType.REFERENCE); - break; - case STRING: - confSp.setType(SearchParamType.STRING); - break; - case TOKEN: - confSp.setType(SearchParamType.TOKEN); - break; - case URI: - confSp.setType(SearchParamType.URI); - break; - case HAS: - // Shouldn't happen - break; + case COMPOSITE: + confSp.setType(SearchParamType.COMPOSITE); + break; + case DATE: + confSp.setType(SearchParamType.DATE); + break; + case NUMBER: + confSp.setType(SearchParamType.NUMBER); + break; + case QUANTITY: + confSp.setType(SearchParamType.QUANTITY); + break; + case REFERENCE: + confSp.setType(SearchParamType.REFERENCE); + break; + case STRING: + confSp.setType(SearchParamType.STRING); + break; + case TOKEN: + confSp.setType(SearchParamType.TOKEN); + break; + case URI: + confSp.setType(SearchParamType.URI); + break; + case HAS: + // Shouldn't happen + break; } - + } - + } } massage(retVal); - + retVal.getImplementation().setDescription(myImplementationDescription); myCachedValue = retVal; return retVal; @@ -151,7 +158,11 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se public boolean isIncludeResourceCounts() { return myIncludeResourceCounts; } - + + public void setIncludeResourceCounts(boolean theIncludeResourceCounts) { + myIncludeResourceCounts = theIncludeResourceCounts; + } + /** * Subclasses may override */ @@ -168,10 +179,6 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se myImplementationDescription = theImplDesc; } - public void setIncludeResourceCounts(boolean theIncludeResourceCounts) { - myIncludeResourceCounts = theIncludeResourceCounts; - } - @Override public void setRestfulServer(RestfulServer theRestfulServer) { this.myRestfulServer = theRestfulServer; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaConformanceProviderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaConformanceProviderR4.java index 0bf42f6cb5e..f47b7501a58 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaConformanceProviderR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaConformanceProviderR4.java @@ -23,6 +23,7 @@ import java.util.*; import javax.servlet.http.HttpServletRequest; +import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.CapabilityStatement.*; import org.hl7.fhir.r4.model.Enumerations.SearchParamType; @@ -41,6 +42,7 @@ public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.S private volatile CapabilityStatement myCachedValue; private DaoConfig myDaoConfig; + private ISearchParamRegistry mySearchParamRegistry; private String myImplementationDescription; private boolean myIncludeResourceCounts; private RestfulServer myRestfulServer; @@ -66,6 +68,11 @@ public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.S myDaoConfig = theDaoConfig; super.setCache(false); setIncludeResourceCounts(true); + setSearchParamRegistry(theSystemDao.getSearchParamRegistry()); + } + + public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) { + mySearchParamRegistry = theSearchParamRegistry; } @Override @@ -99,7 +106,7 @@ public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.S nextResource.getSearchParam().clear(); String resourceName = nextResource.getType(); RuntimeResourceDefinition resourceDef = myRestfulServer.getFhirContext().getResourceDefinition(resourceName); - Collection searchParams = mySystemDao.getSearchParamsByResourceType(resourceDef); + Collection searchParams = mySearchParamRegistry.getSearchParamsByResourceType(resourceDef); for (RuntimeSearchParam runtimeSp : searchParams) { CapabilityStatementRestResourceSearchParamComponent confSp = nextResource.addSearchParam(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java index aedd0743dc9..12ae5e73bab 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java @@ -26,7 +26,9 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.server.BasePagingProvider; import ca.uhn.fhir.rest.server.IPagingProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +@Service public class DatabaseBackedPagingProvider extends BasePagingProvider implements IPagingProvider { @Autowired diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java index 83e5268973f..ac05422cb60 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java @@ -276,8 +276,8 @@ public class PersistedJpaBundleProvider implements IBundleProvider { protected List toResourceList(ISearchBuilder sb, List pidsSubList) { Set includedPids = new HashSet<>(); if (mySearchEntity.getSearchType() == SearchTypeEnum.SEARCH) { - includedPids.addAll(sb.loadIncludes(myDao, myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated(), myUuid)); - includedPids.addAll(sb.loadIncludes(myDao, myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated(), myUuid)); + includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated(), myUuid)); + includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated(), myUuid)); } // Execute the query and make sure we return distinct results diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java index 478ad89ef53..7316bf0608d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java @@ -47,11 +47,15 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.*; +import org.springframework.data.domain.AbstractPageRequest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.orm.jpa.JpaDialect; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.vendor.HibernateJpaDialect; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; +import org.springframework.stereotype.Component; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; @@ -68,6 +72,7 @@ import java.util.concurrent.*; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +@Component("mySearchCoordinatorSvc") public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { public static final int DEFAULT_SYNC_SIZE = 250; @@ -240,8 +245,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { }); } catch (Exception e) { ourLog.warn("Failed to activate search: {}", e.toString()); - // FIXME: aaaaa - ourLog.info("Failed to activate search", e); + ourLog.trace("Failed to activate search", e); return Optional.empty(); } } @@ -313,8 +317,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { * individually for pages as we return them to clients */ final Set includedPids = new HashSet<>(); - includedPids.addAll(sb.loadIncludes(theCallingDao, myContext, myEntityManager, pids, theParams.getRevIncludes(), true, theParams.getLastUpdated(), "(synchronous)")); - includedPids.addAll(sb.loadIncludes(theCallingDao, myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated(), "(synchronous)")); + includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getRevIncludes(), true, theParams.getLastUpdated(), "(synchronous)")); + includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated(), "(synchronous)")); List resources = new ArrayList<>(); sb.loadResourcesByPid(pids, resources, includedPids, false, myEntityManager, myContext, theCallingDao); @@ -513,10 +517,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { * user has requested resources 0-60, then they would get 0-50 back but the search * coordinator would then stop searching.SearchCoordinatorSvcImplTest */ - // FIXME: aaaaaaaa -// List remainingResources = SearchCoordinatorSvcImpl.this.getResources(mySearch.getUuid(), mySyncedPids.size(), theToIndex); -// ourLog.debug("Adding {} resources to the existing {} synced resource IDs", remainingResources.size(), mySyncedPids.size()); -// mySyncedPids.addAll(remainingResources); keepWaiting = false; break; case FAILED: diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java index 00a046f9a07..add221e8944 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/StaleSearchDeletingSvcImpl.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.search; * 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. @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao; import ca.uhn.fhir.jpa.dao.data.ISearchResultDao; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.dstu3.model.InstantType; import org.springframework.beans.factory.annotation.Autowired; @@ -36,13 +37,17 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; import java.util.Date; +import java.util.List; /** * Deletes old searches */ +// +// NOTE: This is not a @Service because we manually instantiate +// it in BaseConfig. This is so that we can override the definition +// in Smile. +// public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { public static final long DEFAULT_CUTOFF_SLACK = 10 * DateUtils.MILLIS_PER_SECOND; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StaleSearchDeletingSvcImpl.class); @@ -51,7 +56,10 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { * DELETE FROM foo WHERE params IN (aaaa) * type query and this can fail if we have 1000s of params */ - public static int ourMaximumResultsToDelete = 500; + public static final int DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_STMT = 500; + public static final int DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_PAS = 20000; + private static int ourMaximumResultsToDeleteInOneStatement = DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_STMT; + private static int ourMaximumResultsToDeleteInOnePass = DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_PAS; private static Long ourNowForUnitTests; /* * We give a bit of extra leeway just to avoid race conditions where a query result @@ -69,8 +77,6 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { private ISearchResultDao mySearchResultDao; @Autowired private PlatformTransactionManager myTransactionManager; - @PersistenceContext() - private EntityManager myEntityManager; private void deleteSearch(final Long theSearchPid) { mySearchDao.findById(theSearchPid).ifPresent(searchToDelete -> { @@ -84,10 +90,14 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { * huge deal to be only partially deleting search results. They'll get deleted * eventually */ - int max = ourMaximumResultsToDelete; + int max = ourMaximumResultsToDeleteInOnePass; Slice resultPids = mySearchResultDao.findForSearch(PageRequest.of(0, max), searchToDelete.getId()); if (resultPids.hasContent()) { - mySearchResultDao.deleteByIds(resultPids.getContent()); + List> partitions = Lists.partition(resultPids.getContent(), ourMaximumResultsToDeleteInOneStatement); + for (List nextPartition : partitions) { + mySearchResultDao.deleteByIds(nextPartition); + } + } // Only delete if we don't have results left in this search @@ -158,9 +168,14 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc { myCutoffSlack = theCutoffSlack; } + @VisibleForTesting + public static void setMaximumResultsToDeleteInOnePassForUnitTest(int theMaximumResultsToDeleteInOnePass) { + ourMaximumResultsToDeleteInOnePass = theMaximumResultsToDeleteInOnePass; + } + @VisibleForTesting public static void setMaximumResultsToDeleteForUnitTest(int theMaximumResultsToDelete) { - ourMaximumResultsToDelete = theMaximumResultsToDelete; + ourMaximumResultsToDeleteInOneStatement = theMaximumResultsToDelete; } private static long now() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/IResourceReindexingSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/IResourceReindexingSvc.java index 571a78fee8f..1384bf47df8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/IResourceReindexingSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/IResourceReindexingSvc.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.search.reindex; * 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. @@ -24,17 +24,24 @@ public interface IResourceReindexingSvc { /** * Marks all indexes as needing fresh indexing + * + * @return Returns the job ID */ - void markAllResourcesForReindexing(); + Long markAllResourcesForReindexing(); /** * Marks all indexes of the given type as needing fresh indexing + * + * @return Returns the job ID */ - void markAllResourcesForReindexing(String theType); + Long markAllResourcesForReindexing(String theType); /** * Called automatically by the job scheduler - * + */ + void scheduleReindexingPass(); + + /** * @return Returns null if the system did not attempt to perform a pass because one was * already proceeding. Otherwise, returns the number of resources affected. */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java index fe5dc23160f..47a7f30e4e8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.search.reindex; * 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. @@ -73,6 +73,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { private static final Date BEGINNING_OF_TIME = new Date(0); private static final Logger ourLog = LoggerFactory.getLogger(ResourceReindexingSvcImpl.class); + private static final int PASS_SIZE = 25000; private final ReentrantLock myIndexingLock = new ReentrantLock(); @Autowired private IResourceReindexJobDao myReindexJobDao; @@ -149,13 +150,13 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { @Override @Transactional(Transactional.TxType.REQUIRED) - public void markAllResourcesForReindexing() { - markAllResourcesForReindexing(null); + public Long markAllResourcesForReindexing() { + return markAllResourcesForReindexing(null); } @Override @Transactional(Transactional.TxType.REQUIRED) - public void markAllResourcesForReindexing(String theType) { + public Long markAllResourcesForReindexing(String theType) { String typeDesc; if (isNotBlank(theType)) { myReindexJobDao.markAllOfTypeAsDeleted(theType); @@ -171,11 +172,19 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { job = myReindexJobDao.saveAndFlush(job); ourLog.info("Marking all resources of type {} for reindexing - Got job ID[{}]", typeDesc, job.getId()); + return job.getId(); } @Override @Transactional(Transactional.TxType.NEVER) @Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND) + public void scheduleReindexingPass() { + runReindexingPass(); + } + + + @Override + @Transactional(Transactional.TxType.NEVER) public Integer runReindexingPass() { if (myDaoConfig.isSchedulingDisabled()) { return null; @@ -223,10 +232,16 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { Collection jobs = myTxTemplate.execute(t -> myReindexJobDao.findAll(PageRequest.of(0, 10), false)); assert jobs != null; + if (jobs.size() > 0) { + ourLog.info("Running {} reindex jobs: {}", jobs.size(), jobs); + } else { + ourLog.debug("Running {} reindex jobs: {}", jobs.size(), jobs); + } + int count = 0; for (ResourceReindexJobEntity next : jobs) { - if (next.getThresholdHigh().getTime() < System.currentTimeMillis()) { + if (next.getThresholdLow() != null && next.getThresholdLow().getTime() >= next.getThresholdHigh().getTime()) { markJobAsDeleted(next); continue; } @@ -236,9 +251,10 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { return count; } - private void markJobAsDeleted(ResourceReindexJobEntity next) { + private void markJobAsDeleted(ResourceReindexJobEntity theJob) { + ourLog.info("Marking reindexing job ID[{}] as deleted", theJob.getId()); myTxTemplate.execute(t -> { - myReindexJobDao.markAsDeletedById(next.getId()); + myReindexJobDao.markAsDeletedById(theJob.getId()); return null; }); } @@ -259,8 +275,9 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { Date high = theJob.getThresholdHigh(); // Query for resources within threshold + StopWatch pageSw = new StopWatch(); Slice range = myTxTemplate.execute(t -> { - PageRequest page = PageRequest.of(0, 10000); + PageRequest page = PageRequest.of(0, PASS_SIZE); if (isNotBlank(theJob.getResourceType())) { return myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(page, theJob.getResourceType(), low, high); } else { @@ -269,6 +286,13 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { }); Validate.notNull(range); int count = range.getNumberOfElements(); + ourLog.info("Loaded {} resources for reindexing in {}", count, pageSw.toString()); + + // If we didn't find any results at all, mark as deleted + if (count == 0) { + markJobAsDeleted(theJob); + return 0; + } // Submit each resource requiring reindexing List> futures = range @@ -277,7 +301,6 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { .collect(Collectors.toList()); Date latestDate = null; - boolean haveMultipleDates = false; for (Future next : futures) { Date nextDate; try { @@ -293,29 +316,22 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { } if (nextDate != null) { - if (latestDate != null) { - if (latestDate.getTime() != nextDate.getTime()) { - haveMultipleDates = true; - } - } if (latestDate == null || latestDate.getTime() < nextDate.getTime()) { latestDate = new Date(nextDate.getTime()); } } } - // Just in case we end up in some sort of infinite loop. This shouldn't happen, and couldn't really - // happen unless there were 10000 resources with the exact same update time down to the - // millisecond. + Validate.notNull(latestDate); Date newLow; - if (latestDate == null) { - markJobAsDeleted(theJob); - return 0; - } if (latestDate.getTime() == low.getTime()) { - ourLog.error("Final pass time for reindex JOB[{}] has same ending low value: {}", theJob.getId(), latestDate); - newLow = new Date(latestDate.getTime() + 1); - } else if (!haveMultipleDates) { + if (count == PASS_SIZE) { + // Just in case we end up in some sort of infinite loop. This shouldn't happen, and couldn't really + // happen unless there were 10000 resources with the exact same update time down to the + // millisecond. + ourLog.error("Final pass time for reindex JOB[{}] has same ending low value: {}", theJob.getId(), latestDate); + } + newLow = new Date(latestDate.getTime() + 1); } else { newLow = latestDate; @@ -323,10 +339,13 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { myTxTemplate.execute(t -> { myReindexJobDao.setThresholdLow(theJob.getId(), newLow); + Integer existingCount = myReindexJobDao.getReindexCount(theJob.getId()).orElse(0); + int newCount = existingCount + counter.get(); + myReindexJobDao.setReindexCount(theJob.getId(), newCount); return null; }); - ourLog.info("Completed pass of reindex JOB[{}] - Indexed {} resources in {} ({} / sec) - Have indexed until: {}", theJob.getId(), count, sw.toString(), sw.formatThroughput(count, TimeUnit.SECONDS), theJob.getThresholdLow()); + ourLog.info("Completed pass of reindex JOB[{}] - Indexed {} resources in {} ({} / sec) - Have indexed until: {}", theJob.getId(), count, sw.toString(), sw.formatThroughput(count, TimeUnit.SECONDS), newLow); return counter.get(); } @@ -450,6 +469,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { return e; } }); + } catch (ResourceVersionConflictException e) { /* * We reindex in multiple threads, so it's technically possible that two threads try @@ -458,7 +478,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { * not get this error, so we'll let the other one fail and try * again later. */ - ourLog.info("Failed to reindex {} because of a version conflict. Leaving in unindexed state: {}", e.getMessage()); + ourLog.info("Failed to reindex because of a version conflict. Leaving in unindexed state: {}", e.getMessage()); reindexFailure = null; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/warm/CacheWarmingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/warm/CacheWarmingSvcImpl.java index bfca9c536e4..721e7ac0b8e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/warm/CacheWarmingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/warm/CacheWarmingSvcImpl.java @@ -23,10 +23,15 @@ package ca.uhn.fhir.jpa.search.warm; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.DaoRegistry; +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.dao.MatchUrlService; import ca.uhn.fhir.parser.DataFormatException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.ArrayList; @@ -34,6 +39,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +@Component public class CacheWarmingSvcImpl implements ICacheWarmingSvc { @Autowired @@ -43,6 +49,8 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc { private FhirContext myCtx; @Autowired private DaoRegistry myDaoRegistry; + @Autowired + private MatchUrlService myMatchUrlService; @Override @Scheduled(fixedDelay = 1000) @@ -72,7 +80,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc { RuntimeResourceDefinition resourceDef = parseUrlResourceType(myCtx, nextUrl); IFhirResourceDao callingDao = myDaoRegistry.getResourceDao(resourceDef.getName()); String queryPart = parseWarmUrlParamPart(nextUrl); - SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(callingDao, myCtx, queryPart, resourceDef); + SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(queryPart, resourceDef); callingDao.search(responseCriteriaUrl); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java index 7ffeef7fa45..4a348c97b1d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java @@ -25,10 +25,12 @@ import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao; import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.SearchParamPresent; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import java.util.*; import java.util.Map.Entry; +@Service public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc { @Autowired diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDeliverySubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDeliverySubscriber.java index 461cb3a798b..e6bc4412614 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDeliverySubscriber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDeliverySubscriber.java @@ -20,21 +20,19 @@ package ca.uhn.fhir.jpa.subscription; * #L% */ -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Scope; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; +import org.springframework.stereotype.Component; public abstract class BaseSubscriptionDeliverySubscriber extends BaseSubscriptionSubscriber { private static final Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionDeliverySubscriber.class); - public BaseSubscriptionDeliverySubscriber(IFhirResourceDao theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) { - super(theSubscriptionDao, theChannelType, theSubscriptionInterceptor); + public BaseSubscriptionDeliverySubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) { + super(theChannelType, theSubscriptionInterceptor); } @Override @@ -59,21 +57,6 @@ public abstract class BaseSubscriptionDeliverySubscriber extends BaseSubscriptio return; } - // Load the resource - IIdType payloadId = msg.getPayloadId(getContext()); - Class type = getContext().getResourceDefinition(payloadId.getResourceType()).getImplementingClass(); - IFhirResourceDao dao = getSubscriptionInterceptor().getDao(type); - IBaseResource loadedPayload; - try { - loadedPayload = dao.read(payloadId); - } catch (ResourceNotFoundException e) { - // This can happen if a last minute failure happens when saving a resource, - // eg a constraint causes the transaction to roll back on commit - ourLog.warn("Unable to find resource {} - Aborting delivery", payloadId.getValue()); - return; - } - msg.setPayload(getContext(), loadedPayload); - handleMessage(msg); } catch (Exception e) { String msg = "Failure handling subscription payload for subscription: " + subscriptionId; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java index 4ab9affa23f..2ffea8ddcc6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java @@ -1,42 +1,27 @@ package ca.uhn.fhir.jpa.subscription; -/*- - * #%L - * HAPI FHIR JPA Server - * %% - * Copyright (C) 2014 - 2018 University Health Network - * %% - * 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.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.config.BaseConfig; +import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.MatchUrlService; import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; -import ca.uhn.fhir.jpa.subscription.matcher.ISubscriptionMatcher; +import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl; +import ca.uhn.fhir.jpa.subscription.matcher.SubscriptionMatcherCompositeInMemoryDatabase; import ca.uhn.fhir.jpa.subscription.matcher.SubscriptionMatcherDatabase; import ca.uhn.fhir.jpa.util.JpaConstants; -import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import ca.uhn.fhir.util.StopWatch; import com.google.common.annotations.VisibleForTesting; @@ -52,6 +37,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -73,11 +59,32 @@ import javax.annotation.PreDestroy; import java.util.*; import java.util.concurrent.*; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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 abstract class BaseSubscriptionInterceptor extends ServerOperationInterceptorAdapter { static final String SUBSCRIPTION_STATUS = "Subscription.status"; static final String SUBSCRIPTION_TYPE = "Subscription.channel.type"; private static final Integer MAX_SUBSCRIPTION_RESULTS = 1000; + private static boolean ourForcePayloadEncodeAndDecodeForUnitTests; private final Object myInitSubscriptionsLock = new Object(); private SubscribableChannel myProcessingChannel; private Map myDeliveryChannel; @@ -91,9 +98,6 @@ public abstract class BaseSubscriptionInterceptor exten private Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionInterceptor.class); private ThreadPoolExecutor myDeliveryExecutor; private LinkedBlockingQueue myProcessingExecutorQueue; - private IFhirResourceDao mySubscriptionDao; - @Autowired - private List> myResourceDaos; @Autowired private FhirContext myCtx; @Autowired(required = false) @@ -104,7 +108,16 @@ public abstract class BaseSubscriptionInterceptor exten @Autowired @Qualifier(BaseConfig.TASK_EXECUTOR_NAME) private AsyncTaskExecutor myAsyncTaskExecutor; - private Map, IFhirResourceDao> myResourceTypeToDao; + @Autowired + private SubscriptionMatcherCompositeInMemoryDatabase mySubscriptionMatcherCompositeInMemoryDatabase; + @Autowired + private SubscriptionMatcherDatabase mySubscriptionMatcherDatabase; + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired + private BeanFactory beanFactory; + @Autowired + private MatchUrlService myMatchUrlService; private Semaphore myInitSubscriptionsSemaphore = new Semaphore(1); /** @@ -286,26 +299,6 @@ public abstract class BaseSubscriptionInterceptor exten public abstract Subscription.SubscriptionChannelType getChannelType(); - // TODO KHS move out - @SuppressWarnings("unchecked") - public IFhirResourceDao getDao(Class theType) { - if (myResourceTypeToDao == null) { - Map, IFhirResourceDao> theResourceTypeToDao = new HashMap<>(); - for (IFhirResourceDao next : myResourceDaos) { - theResourceTypeToDao.put(next.getResourceType(), next); - } - - if (this instanceof IFhirResourceDao) { - IFhirResourceDao thiz = (IFhirResourceDao) this; - theResourceTypeToDao.put(thiz.getResourceType(), thiz); - } - - myResourceTypeToDao = theResourceTypeToDao; - } - - return (IFhirResourceDao) myResourceTypeToDao.get(theType); - } - protected MessageChannel getDeliveryChannel(CanonicalSubscription theSubscription) { return mySubscribableChannel.get(theSubscription.getIdElement(myCtx).getIdPart()); } @@ -335,10 +328,6 @@ public abstract class BaseSubscriptionInterceptor exten myProcessingChannel = theProcessingChannel; } - protected IFhirResourceDao getSubscriptionDao() { - return mySubscriptionDao; - } - public List getRegisteredSubscriptions() { return new ArrayList<>(myIdToSubscription.values()); } @@ -378,7 +367,8 @@ public abstract class BaseSubscriptionInterceptor exten RequestDetails req = new ServletSubRequestDetails(); req.setSubRequest(true); - IBundleProvider subscriptionBundleList = getSubscriptionDao().search(map, req); + IFhirResourceDao subscriptionDao = myDaoRegistry.getResourceDao("Subscription"); + IBundleProvider subscriptionBundleList = subscriptionDao.search(map, req); if (subscriptionBundleList.size() >= MAX_SUBSCRIPTION_RESULTS) { ourLog.error("Currently over " + MAX_SUBSCRIPTION_RESULTS + " subscriptions. Some subscriptions have not been loaded."); } @@ -436,19 +426,14 @@ public abstract class BaseSubscriptionInterceptor exten protected void registerSubscriptionCheckingSubscriber() { if (mySubscriptionCheckingSubscriber == null) { - ISubscriptionMatcher subscriptionMatcher = new SubscriptionMatcherDatabase(getSubscriptionDao(), this); - mySubscriptionCheckingSubscriber = new SubscriptionCheckingSubscriber(getSubscriptionDao(), getChannelType(), this, subscriptionMatcher ); + mySubscriptionCheckingSubscriber = beanFactory.getBean(SubscriptionCheckingSubscriber.class, getChannelType(), this, mySubscriptionMatcherCompositeInMemoryDatabase); } getProcessingChannel().subscribe(mySubscriptionCheckingSubscriber); } @Override public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { - ResourceModifiedMessage msg = new ResourceModifiedMessage(); - msg.setId(theResource.getIdElement()); - msg.setOperationType(ResourceModifiedMessage.OperationTypeEnum.CREATE); - msg.setNewPayload(myCtx, theResource); - submitResourceModified(msg); + submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); } @Override @@ -465,10 +450,17 @@ public abstract class BaseSubscriptionInterceptor exten } void submitResourceModifiedForUpdate(IBaseResource theNewResource) { + submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); + } + + private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) { ResourceModifiedMessage msg = new ResourceModifiedMessage(); msg.setId(theNewResource.getIdElement()); - msg.setOperationType(ResourceModifiedMessage.OperationTypeEnum.UPDATE); + msg.setOperationType(theOperationType); msg.setNewPayload(myCtx, theNewResource); + if (ourForcePayloadEncodeAndDecodeForUnitTests) { + msg.clearPayloadDecoded(); + } submitResourceModified(msg); } @@ -501,10 +493,6 @@ public abstract class BaseSubscriptionInterceptor exten myCtx = theCtx; } - public void setResourceDaos(List> theResourceDaos) { - myResourceDaos = theResourceDaos; - } - @VisibleForTesting public void setTxManager(PlatformTransactionManager theTxManager) { myTxManager = theTxManager; @@ -512,15 +500,6 @@ public abstract class BaseSubscriptionInterceptor exten @PostConstruct public void start() { - for (IFhirResourceDao next : myResourceDaos) { - if (next.getResourceType() != null) { - if (myCtx.getResourceDefinition(next.getResourceType()).getName().equals("Subscription")) { - mySubscriptionDao = next; - } - } - } - Validate.notNull(mySubscriptionDao); - if (myCtx.getVersion().getVersion() == FhirVersionEnum.R4) { Validate.notNull(myEventDefinitionDaoR4); } @@ -555,7 +534,8 @@ public abstract class BaseSubscriptionInterceptor exten } if (mySubscriptionActivatingSubscriber == null) { - mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(getSubscriptionDao(), getChannelType(), this, myTxManager, myAsyncTaskExecutor); + IFhirResourceDao subscriptionDao = myDaoRegistry.getResourceDao("Subscription"); + mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(subscriptionDao, getChannelType(), this, myTxManager, myAsyncTaskExecutor); } registerSubscriptionCheckingSubscriber(); @@ -619,5 +599,31 @@ public abstract class BaseSubscriptionInterceptor exten return myIdToSubscription.remove(subscriptionId); } + public IFhirResourceDao getSubscriptionDao() { + return myDaoRegistry.getResourceDao("Subscription"); + } + public IFhirResourceDao getDao(Class type) { + return myDaoRegistry.getResourceDao(type); + } + + public void setResourceDaos(List theResourceDaos) { + myDaoRegistry.setResourceDaos(theResourceDaos); + } + + public void validateCriteria(final S theResource) { + CanonicalSubscription subscription = canonicalize(theResource); + String criteria = subscription.getCriteriaString(); + try { + RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myCtx, criteria); + myMatchUrlService.translateMatchUrl(criteria, resourceDef); + } catch (InvalidRequestException e) { + throw new UnprocessableEntityException("Invalid subscription criteria submitted: " + criteria + " " + e.getMessage()); + } + } + + @VisibleForTesting + public static void setForcePayloadEncodeAndDecodeForUnitTests(boolean theForcePayloadEncodeAndDecodeForUnitTests) { + ourForcePayloadEncodeAndDecodeForUnitTests = theForcePayloadEncodeAndDecodeForUnitTests; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionSubscriber.java index bc2e91c4ff1..256466b4eb1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionSubscriber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionSubscriber.java @@ -21,25 +21,40 @@ package ca.uhn.fhir.jpa.subscription; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import org.hl7.fhir.r4.model.Subscription; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.MessageHandler; +import javax.annotation.PostConstruct; + public abstract class BaseSubscriptionSubscriber implements MessageHandler { - private final IFhirResourceDao mySubscriptionDao; private final Subscription.SubscriptionChannelType myChannelType; private final BaseSubscriptionInterceptor mySubscriptionInterceptor; + @Autowired + DaoRegistry myDaoRegistry; + private IFhirResourceDao mySubscriptionDao; /** * Constructor */ - public BaseSubscriptionSubscriber(IFhirResourceDao theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) { - mySubscriptionDao = theSubscriptionDao; + public BaseSubscriptionSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) { myChannelType = theChannelType; mySubscriptionInterceptor = theSubscriptionInterceptor; } + @SuppressWarnings("unused") // Don't delete, used in Smile + public void setDaoRegistry(DaoRegistry theDaoRegistry) { + myDaoRegistry = theDaoRegistry; + } + + @PostConstruct + public void setSubscriptionDao() { + mySubscriptionDao = myDaoRegistry.getResourceDao("Subscription"); + } + public Subscription.SubscriptionChannelType getChannelType() { return myChannelType; } @@ -71,7 +86,7 @@ public abstract class BaseSubscriptionSubscriber implements MessageHandler { */ static boolean subscriptionTypeApplies(String theSubscriptionChannelTypeCode, Subscription.SubscriptionChannelType theChannelType) { boolean subscriptionTypeApplies = false; - if (theSubscriptionChannelTypeCode != null) { + if (theSubscriptionChannelTypeCode != null) { if (theChannelType.toCode().equals(theSubscriptionChannelTypeCode)) { subscriptionTypeApplies = true; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ISubscriptionTriggeringSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ISubscriptionTriggeringSvc.java new file mode 100644 index 00000000000..c80bcc6b931 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ISubscriptionTriggeringSvc.java @@ -0,0 +1,33 @@ +package ca.uhn.fhir.jpa.subscription; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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.annotation.IdParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.UriParam; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IIdType; + +import java.util.List; + +public interface ISubscriptionTriggeringSvc { + IBaseParameters triggerSubscription(List theResourceIds, List theSearchUrls, @IdParam IIdType theSubscriptionId); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryMessage.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryMessage.java index ca1ffa7ad7d..e4a495d4bc0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryMessage.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryMessage.java @@ -27,6 +27,8 @@ import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public class ResourceDeliveryMessage { @@ -37,6 +39,8 @@ public class ResourceDeliveryMessage { private transient CanonicalSubscription mySubscription; @JsonProperty("subscription") private String mySubscriptionString; + @JsonProperty("payload") + private String myPayloadString; @JsonIgnore private transient IBaseResource myPayload; @JsonProperty("payloadId") @@ -60,8 +64,12 @@ public class ResourceDeliveryMessage { } public IBaseResource getPayload(FhirContext theCtx) { - Validate.notNull(myPayload); - return myPayload; + IBaseResource retVal = myPayload; + if (retVal == null && isNotBlank(myPayloadString)) { + retVal = theCtx.newJsonParser().parseResource(myPayloadString); + myPayload = retVal; + } + return retVal; } public IIdType getPayloadId(FhirContext theCtx) { @@ -88,6 +96,7 @@ public class ResourceDeliveryMessage { public void setPayload(FhirContext theCtx, IBaseResource thePayload) { myPayload = thePayload; + myPayloadString = theCtx.newJsonParser().encodeResourceToString(thePayload); } public void setPayloadId(IIdType thePayloadId) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java index caaccfbcaee..57cf3f042cb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription; * 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. @@ -28,6 +28,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public class ResourceModifiedMessage { @@ -44,10 +46,16 @@ public class ResourceModifiedMessage { */ @JsonProperty(value = "subscriptionId", required = false) private String mySubscriptionId; - @JsonProperty("newPayload") - private String myNewPayloadEncoded; + @JsonProperty("payload") + private String myPayload; + @JsonProperty("payloadId") + private String myPayloadId; @JsonIgnore - private transient IBaseResource myNewPayload; + private transient IBaseResource myPayloadDecoded; + + public String getPayloadId() { + return myPayloadId; + } public String getSubscriptionId() { return mySubscriptionId; @@ -66,10 +74,10 @@ public class ResourceModifiedMessage { } public IBaseResource getNewPayload(FhirContext theCtx) { - if (myNewPayload == null && myNewPayloadEncoded != null) { - myNewPayload = theCtx.newJsonParser().parseResource(myNewPayloadEncoded); + if (myPayloadDecoded == null && isNotBlank(myPayload)) { + myPayloadDecoded = theCtx.newJsonParser().parseResource(myPayload); } - return myNewPayload; + return myPayloadDecoded; } public OperationTypeEnum getOperationType() { @@ -88,8 +96,19 @@ public class ResourceModifiedMessage { } public void setNewPayload(FhirContext theCtx, IBaseResource theNewPayload) { - myNewPayload = theNewPayload; - myNewPayloadEncoded = theCtx.newJsonParser().encodeResourceToString(theNewPayload); + myPayload = theCtx.newJsonParser().encodeResourceToString(theNewPayload); + myPayloadId = theNewPayload.getIdElement().toUnqualified().getValue(); + myPayloadDecoded = theNewPayload; + } + + /** + * This is mostly useful for unit tests - Clear the decoded payload so that + * we force the encoded version to be used later. This proves that we get the same + * behaviour in environments with serializing queues as we do with in-memory + * queues. + */ + public void clearPayloadDecoded() { + myPayloadDecoded = null; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java index 209f5cb23bc..77e004b5506 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java @@ -162,16 +162,16 @@ public class SubscriptionActivatingSubscriber { @SuppressWarnings("EnumSwitchStatementWhichMissesCases") public void handleMessage(ResourceModifiedMessage.OperationTypeEnum theOperationType, IIdType theId, final IBaseResource theSubscription) throws MessagingException { - + if (!theId.getResourceType().equals("Subscription")) { + return; + } switch (theOperationType) { case DELETE: mySubscriptionInterceptor.unregisterSubscription(theId); - return; + break; case CREATE: case UPDATE: - if (!theId.getResourceType().equals("Subscription")) { - return; - } + mySubscriptionInterceptor.validateCriteria(theSubscription); activateAndRegisterSubscriptionIfRequiredInTransaction(theSubscription); break; default: diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionCheckingSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionCheckingSubscriber.java index 858ea09b1c8..a8716e03519 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionCheckingSubscriber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionCheckingSubscriber.java @@ -1,16 +1,29 @@ package ca.uhn.fhir.jpa.subscription; -import java.util.List; - +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; +import ca.uhn.fhir.jpa.dao.MatchUrlService; +import ca.uhn.fhir.jpa.subscription.matcher.ISubscriptionMatcher; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessagingException; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; /*- * #%L @@ -32,24 +45,18 @@ import org.springframework.messaging.MessagingException; * #L% */ -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; -import ca.uhn.fhir.jpa.subscription.matcher.ISubscriptionMatcher; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.api.server.RequestDetails; - -import static org.apache.commons.lang3.StringUtils.isNotBlank; - +@Component +@Scope("prototype") public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber { private Logger ourLog = LoggerFactory.getLogger(SubscriptionCheckingSubscriber.class); private final ISubscriptionMatcher mySubscriptionMatcher; - - public SubscriptionCheckingSubscriber(IFhirResourceDao theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor, ISubscriptionMatcher theSubscriptionMatcher) { - super(theSubscriptionDao, theChannelType, theSubscriptionInterceptor); + + @Autowired + private MatchUrlService myMatchUrlService; + + public SubscriptionCheckingSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor, ISubscriptionMatcher theSubscriptionMatcher) { + super(theChannelType, theSubscriptionInterceptor); this.mySubscriptionMatcher = theSubscriptionMatcher; } @@ -77,11 +84,10 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber { IIdType id = msg.getId(getContext()); String resourceType = id.getResourceType(); - String resourceId = id.getIdPart(); List subscriptions = getSubscriptionInterceptor().getRegisteredSubscriptions(); - ourLog.trace("Testing {} subscriptions for applicability"); + ourLog.trace("Testing {} subscriptions for applicability", subscriptions.size()); for (CanonicalSubscription nextSubscription : subscriptions) { @@ -112,7 +118,7 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber { continue; } - if (!mySubscriptionMatcher.match(nextCriteriaString, msg)) { + if (!mySubscriptionMatcher.match(nextCriteriaString, msg).matched()) { continue; } @@ -148,7 +154,7 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber { */ protected IBundleProvider performSearch(String theCriteria) { RuntimeResourceDefinition responseResourceDef = getSubscriptionDao().validateCriteriaAndReturnResourceDefinition(theCriteria); - SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(getSubscriptionDao(), getSubscriptionDao().getContext(), theCriteria, responseResourceDef); + SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef); RequestDetails req = new ServletSubRequestDetails(); req.setSubRequest(true); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java new file mode 100644 index 00000000000..1e3936c7a6e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java @@ -0,0 +1,463 @@ +package ca.uhn.fhir.jpa.subscription; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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 ca.uhn.fhir.jpa.dao.DaoRegistry; +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.MatchUrlService; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; +import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.api.CacheControlDirective; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.util.ParametersUtil; +import ca.uhn.fhir.util.StopWatch; +import ca.uhn.fhir.util.ValidateUtil; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.hl7.fhir.instance.model.IdType; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +import static ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider.RESOURCE_ID; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Service +public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc, ApplicationContextAware { + + public static final int DEFAULT_MAX_SUBMIT = 10000; + private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTriggeringProvider.class); + private final List myActiveJobs = new ArrayList<>(); + @Autowired + private FhirContext myFhirContext; + @Autowired + private DaoRegistry myDaoRegistry; + private List> mySubscriptionInterceptorList; + private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT; + @Autowired + private ISearchCoordinatorSvc mySearchCoordinatorSvc; + @Autowired + private MatchUrlService myMatchUrlService; + private ApplicationContext myAppCtx; + private ExecutorService myExecutorService; + + @Override + public IBaseParameters triggerSubscription(List theResourceIds, List theSearchUrls, @IdParam IIdType theSubscriptionId) { + if (mySubscriptionInterceptorList.isEmpty()) { + throw new PreconditionFailedException("Subscription processing not active on this server"); + } + + // Throw a 404 if the subscription doesn't exist + if (theSubscriptionId != null) { + IFhirResourceDao subscriptionDao = myDaoRegistry.getResourceDao("Subscription"); + IIdType subscriptionId = theSubscriptionId; + if (subscriptionId.hasResourceType() == false) { + subscriptionId = subscriptionId.withResourceType("Subscription"); + } + subscriptionDao.read(subscriptionId); + } + + List resourceIds = ObjectUtils.defaultIfNull(theResourceIds, Collections.emptyList()); + List searchUrls = ObjectUtils.defaultIfNull(theSearchUrls, Collections.emptyList()); + + // Make sure we have at least one resource ID or search URL + if (resourceIds.size() == 0 && searchUrls.size() == 0) { + throw new InvalidRequestException("No resource IDs or search URLs specified for triggering"); + } + + // Resource URLs must be compete + for (UriParam next : resourceIds) { + IdType resourceId = new IdType(next.getValue()); + ValidateUtil.isTrueOrThrowInvalidRequest(resourceId.hasResourceType(), RESOURCE_ID + " parameter must have resource type"); + ValidateUtil.isTrueOrThrowInvalidRequest(resourceId.hasIdPart(), RESOURCE_ID + " parameter must have resource ID part"); + } + + // Search URLs must be valid + for (StringParam next : searchUrls) { + if (next.getValue().contains("?") == false) { + throw new InvalidRequestException("Search URL is not valid (must be in the form \"[resource type]?[optional params]\")"); + } + } + + SubscriptionTriggeringJobDetails jobDetails = new SubscriptionTriggeringJobDetails(); + jobDetails.setJobId(UUID.randomUUID().toString()); + jobDetails.setRemainingResourceIds(resourceIds.stream().map(UriParam::getValue).collect(Collectors.toList())); + jobDetails.setRemainingSearchUrls(searchUrls.stream().map(StringParam::getValue).collect(Collectors.toList())); + if (theSubscriptionId != null) { + jobDetails.setSubscriptionId(theSubscriptionId.toUnqualifiedVersionless().getValue()); + } + + // Submit job for processing + synchronized (myActiveJobs) { + myActiveJobs.add(jobDetails); + } + ourLog.info("Subscription triggering requested for {} resource and {} search - Gave job ID: {}", resourceIds.size(), searchUrls.size(), jobDetails.getJobId()); + + // Create a parameters response + IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext); + IPrimitiveType value = (IPrimitiveType) myFhirContext.getElementDefinition("string").newInstance(); + value.setValueAsString("Subscription triggering job submitted as JOB ID: " + jobDetails.myJobId); + ParametersUtil.addParameterToParameters(myFhirContext, retVal, "information", value); + return retVal; + } + + @Scheduled(fixedDelay = DateUtils.MILLIS_PER_SECOND) + public void runDeliveryPass() { + + synchronized (myActiveJobs) { + if (myActiveJobs.isEmpty()) { + return; + } + + String activeJobIds = myActiveJobs.stream().map(t -> t.getJobId()).collect(Collectors.joining(", ")); + ourLog.info("Starting pass: currently have {} active job IDs: {}", myActiveJobs.size(), activeJobIds); + + SubscriptionTriggeringJobDetails activeJob = myActiveJobs.get(0); + + runJob(activeJob); + + // If the job is complete, remove it from the queue + if (activeJob.getRemainingResourceIds().isEmpty()) { + if (activeJob.getRemainingSearchUrls().isEmpty()) { + if (isBlank(activeJob.myCurrentSearchUuid)) { + myActiveJobs.remove(0); + String remainingJobsMsg = ""; + if (myActiveJobs.size() > 0) { + remainingJobsMsg = "(" + myActiveJobs.size() + " jobs remaining)"; + } + ourLog.info("Subscription triggering job {} is complete{}", activeJob.getJobId(), remainingJobsMsg); + } + } + } + + } + + } + + private void runJob(SubscriptionTriggeringJobDetails theJobDetails) { + StopWatch sw = new StopWatch(); + ourLog.info("Starting pass of subscription triggering job {}", theJobDetails.getJobId()); + + // Submit individual resources + int totalSubmitted = 0; + List>> futures = new ArrayList<>(); + while (theJobDetails.getRemainingResourceIds().size() > 0 && totalSubmitted < myMaxSubmitPerPass) { + totalSubmitted++; + String nextResourceId = theJobDetails.getRemainingResourceIds().remove(0); + Future future = submitResource(theJobDetails.getSubscriptionId(), nextResourceId); + futures.add(Pair.of(nextResourceId, future)); + } + + // Make sure these all succeeded in submitting + if (validateFuturesAndReturnTrueIfWeShouldAbort(futures)) { + return; + } + + // If we don't have an active search started, and one needs to be.. start it + if (isBlank(theJobDetails.getCurrentSearchUuid()) && theJobDetails.getRemainingSearchUrls().size() > 0 && totalSubmitted < myMaxSubmitPerPass) { + String nextSearchUrl = theJobDetails.getRemainingSearchUrls().remove(0); + RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myFhirContext, nextSearchUrl); + String queryPart = nextSearchUrl.substring(nextSearchUrl.indexOf('?')); + String resourceType = resourceDef.getName(); + + IFhirResourceDao callingDao = myDaoRegistry.getResourceDao(resourceType); + SearchParameterMap params = myMatchUrlService.translateMatchUrl(queryPart, resourceDef); + + ourLog.info("Triggering job[{}] is starting a search for {}", theJobDetails.getJobId(), nextSearchUrl); + + IBundleProvider search = mySearchCoordinatorSvc.registerSearch(callingDao, params, resourceType, new CacheControlDirective()); + theJobDetails.setCurrentSearchUuid(search.getUuid()); + theJobDetails.setCurrentSearchResourceType(resourceType); + theJobDetails.setCurrentSearchCount(params.getCount()); + theJobDetails.setCurrentSearchLastUploadedIndex(-1); + } + + // If we have an active search going, submit resources from it + if (isNotBlank(theJobDetails.getCurrentSearchUuid()) && totalSubmitted < myMaxSubmitPerPass) { + int fromIndex = theJobDetails.getCurrentSearchLastUploadedIndex() + 1; + + IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theJobDetails.getCurrentSearchResourceType()); + + int maxQuerySize = myMaxSubmitPerPass - totalSubmitted; + int toIndex = fromIndex + maxQuerySize; + if (theJobDetails.getCurrentSearchCount() != null) { + toIndex = Math.min(toIndex, theJobDetails.getCurrentSearchCount()); + } + ourLog.info("Triggering job[{}] search {} requesting resources {} - {}", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex); + List resourceIds = mySearchCoordinatorSvc.getResources(theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex); + + ourLog.info("Triggering job[{}] delivering {} resources", theJobDetails.getJobId(), resourceIds.size()); + int highestIndexSubmitted = theJobDetails.getCurrentSearchLastUploadedIndex(); + + for (Long next : resourceIds) { + IBaseResource nextResource = resourceDao.readByPid(next); + Future future = submitResource(theJobDetails.getSubscriptionId(), nextResource); + futures.add(Pair.of(nextResource.getIdElement().getIdPart(), future)); + totalSubmitted++; + highestIndexSubmitted++; + } + + if (validateFuturesAndReturnTrueIfWeShouldAbort(futures)) { + return; + } + + theJobDetails.setCurrentSearchLastUploadedIndex(highestIndexSubmitted); + + if (resourceIds.size() == 0 || (theJobDetails.getCurrentSearchCount() != null && toIndex >= theJobDetails.getCurrentSearchCount())) { + ourLog.info("Triggering job[{}] search {} has completed ", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid()); + theJobDetails.setCurrentSearchResourceType(null); + theJobDetails.setCurrentSearchUuid(null); + theJobDetails.setCurrentSearchLastUploadedIndex(-1); + theJobDetails.setCurrentSearchCount(null); + } + } + + ourLog.info("Subscription trigger job[{}] triggered {} resources in {}ms ({} res / second)", theJobDetails.getJobId(), totalSubmitted, sw.getMillis(), sw.getThroughput(totalSubmitted, TimeUnit.SECONDS)); + } + + private boolean validateFuturesAndReturnTrueIfWeShouldAbort(List>> theIdToFutures) { + + for (Pair> next : theIdToFutures) { + String nextDeliveredId = next.getKey(); + try { + Future nextFuture = next.getValue(); + nextFuture.get(); + ourLog.info("Finished redelivering {}", nextDeliveredId); + } catch (Exception e) { + ourLog.error("Failure triggering resource " + nextDeliveredId, e); + return true; + } + } + + // Clear the list since it will potentially get reused + theIdToFutures.clear(); + return false; + } + + private Future submitResource(String theSubscriptionId, String theResourceIdToTrigger) { + org.hl7.fhir.r4.model.IdType resourceId = new org.hl7.fhir.r4.model.IdType(theResourceIdToTrigger); + IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceId.getResourceType()); + IBaseResource resourceToTrigger = dao.read(resourceId); + + return submitResource(theSubscriptionId, resourceToTrigger); + } + + private Future submitResource(String theSubscriptionId, IBaseResource theResourceToTrigger) { + + ourLog.info("Submitting resource {} to subscription {}", theResourceToTrigger.getIdElement().toUnqualifiedVersionless().getValue(), theSubscriptionId); + + ResourceModifiedMessage msg = new ResourceModifiedMessage(); + msg.setId(theResourceToTrigger.getIdElement()); + msg.setOperationType(ResourceModifiedMessage.OperationTypeEnum.UPDATE); + msg.setSubscriptionId(new IdType(theSubscriptionId).toUnqualifiedVersionless().getValue()); + msg.setNewPayload(myFhirContext, theResourceToTrigger); + + return myExecutorService.submit(() -> { + for (int i = 0; ; i++) { + try { + for (BaseSubscriptionInterceptor next : mySubscriptionInterceptorList) { + next.submitResourceModified(msg); + } + break; + } catch (Exception e) { + if (i >= 3) { + throw new InternalErrorException(e); + } + + ourLog.warn("Exception while retriggering subscriptions (going to sleep and retry): {}", e.toString()); + Thread.sleep(1000); + } + } + + return null; + }); + + } + + public void cancelAll() { + synchronized (myActiveJobs) { + myActiveJobs.clear(); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + myAppCtx = applicationContext; + } + + /** + * Sets the maximum number of resources that will be submitted in a single pass + */ + public void setMaxSubmitPerPass(Integer theMaxSubmitPerPass) { + Integer maxSubmitPerPass = theMaxSubmitPerPass; + if (maxSubmitPerPass == null) { + maxSubmitPerPass = DEFAULT_MAX_SUBMIT; + } + Validate.isTrue(maxSubmitPerPass > 0, "theMaxSubmitPerPass must be > 0"); + myMaxSubmitPerPass = maxSubmitPerPass; + } + + @SuppressWarnings("unchecked") + @PostConstruct + public void start() { + mySubscriptionInterceptorList = ObjectUtils.defaultIfNull(mySubscriptionInterceptorList, Collections.emptyList()); + mySubscriptionInterceptorList = new ArrayList<>(); + Collection values1 = myAppCtx.getBeansOfType(BaseSubscriptionInterceptor.class).values(); + Collection> values = (Collection>) values1; + mySubscriptionInterceptorList.addAll(values); + + + LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(1000); + BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() + .namingPattern("SubscriptionTriggering-%d") + .daemon(false) + .priority(Thread.NORM_PRIORITY) + .build(); + RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) { + ourLog.info("Note: Subscription triggering queue is full ({} elements), waiting for a slot to become available!", executorQueue.size()); + StopWatch sw = new StopWatch(); + try { + executorQueue.put(theRunnable); + } catch (InterruptedException theE) { + throw new RejectedExecutionException("Task " + theRunnable.toString() + + " rejected from " + theE.toString()); + } + ourLog.info("Slot become available after {}ms", sw.getMillis()); + } + }; + myExecutorService = new ThreadPoolExecutor( + 0, + 10, + 0L, + TimeUnit.MILLISECONDS, + executorQueue, + threadFactory, + rejectedExecutionHandler); + + } + + private static class SubscriptionTriggeringJobDetails { + + private String myJobId; + private String mySubscriptionId; + private List myRemainingResourceIds; + private List myRemainingSearchUrls; + private String myCurrentSearchUuid; + private Integer myCurrentSearchCount; + private String myCurrentSearchResourceType; + private int myCurrentSearchLastUploadedIndex; + + public Integer getCurrentSearchCount() { + return myCurrentSearchCount; + } + + public void setCurrentSearchCount(Integer theCurrentSearchCount) { + myCurrentSearchCount = theCurrentSearchCount; + } + + public String getCurrentSearchResourceType() { + return myCurrentSearchResourceType; + } + + public void setCurrentSearchResourceType(String theCurrentSearchResourceType) { + myCurrentSearchResourceType = theCurrentSearchResourceType; + } + + public String getJobId() { + return myJobId; + } + + public void setJobId(String theJobId) { + myJobId = theJobId; + } + + public String getSubscriptionId() { + return mySubscriptionId; + } + + public void setSubscriptionId(String theSubscriptionId) { + mySubscriptionId = theSubscriptionId; + } + + public List getRemainingResourceIds() { + return myRemainingResourceIds; + } + + public void setRemainingResourceIds(List theRemainingResourceIds) { + myRemainingResourceIds = theRemainingResourceIds; + } + + public List getRemainingSearchUrls() { + return myRemainingSearchUrls; + } + + public void setRemainingSearchUrls(List theRemainingSearchUrls) { + myRemainingSearchUrls = theRemainingSearchUrls; + } + + public String getCurrentSearchUuid() { + return myCurrentSearchUuid; + } + + public void setCurrentSearchUuid(String theCurrentSearchUuid) { + myCurrentSearchUuid = theCurrentSearchUuid; + } + + public int getCurrentSearchLastUploadedIndex() { + return myCurrentSearchLastUploadedIndex; + } + + public void setCurrentSearchLastUploadedIndex(int theCurrentSearchLastUploadedIndex) { + myCurrentSearchLastUploadedIndex = theCurrentSearchLastUploadedIndex; + } + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSender.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSender.java index 467c62b4090..c4c101e5cca 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSender.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSender.java @@ -20,17 +20,17 @@ package ca.uhn.fhir.jpa.subscription.email; * #L% */ -import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.StopWatch; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.thymeleaf.context.Context; -import org.thymeleaf.spring4.SpringTemplateEngine; -import org.thymeleaf.spring4.dialect.SpringStandardDialect; +import org.thymeleaf.spring5.SpringTemplateEngine; +import org.thymeleaf.spring5.dialect.SpringStandardDialect; import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templateresolver.StringTemplateResolver; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionDeliveringEmailSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionDeliveringEmailSubscriber.java index a7101db1248..cb659226e97 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionDeliveringEmailSubscriber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionDeliveringEmailSubscriber.java @@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.subscription.email; * #L% */ -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDeliverySubscriber; import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.ResourceDeliveryMessage; @@ -28,19 +27,24 @@ import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import static org.apache.commons.lang3.StringUtils.*; +@Component +@Scope("prototype") + public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliverySubscriber { private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringEmailSubscriber.class); private SubscriptionEmailInterceptor mySubscriptionEmailInterceptor; - public SubscriptionDeliveringEmailSubscriber(IFhirResourceDao theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, SubscriptionEmailInterceptor theSubscriptionEmailInterceptor) { - super(theSubscriptionDao, theChannelType, theSubscriptionEmailInterceptor); + public SubscriptionDeliveringEmailSubscriber(Subscription.SubscriptionChannelType theChannelType, SubscriptionEmailInterceptor theSubscriptionEmailInterceptor) { + super(theChannelType, theSubscriptionEmailInterceptor); mySubscriptionEmailInterceptor = theSubscriptionEmailInterceptor; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionEmailInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionEmailInterceptor.java index 270bf3ee9b1..0e3c85db169 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionEmailInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionEmailInterceptor.java @@ -23,11 +23,19 @@ package ca.uhn.fhir.jpa.subscription.email; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.messaging.MessageHandler; +import org.springframework.stereotype.Component; import java.util.Optional; +/** + * Note: If you're going to use this, you need to provide a bean + * of type {@link ca.uhn.fhir.jpa.subscription.email.IEmailSender} + * in your own Spring config + */ public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor { /** @@ -36,11 +44,13 @@ public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor { */ @Autowired(required = false) private IEmailSender myEmailSender; + @Autowired + BeanFactory myBeanFactory; private String myDefaultFromAddress = "noreply@unknown.com"; @Override protected Optional createDeliveryHandler(CanonicalSubscription theSubscription) { - return Optional.of(new SubscriptionDeliveringEmailSubscriber(getSubscriptionDao(), getChannelType(), this)); + return Optional.of(myBeanFactory.getBean(SubscriptionDeliveringEmailSubscriber.class, getChannelType(), this)); } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/CriteriaResourceMatcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/CriteriaResourceMatcher.java new file mode 100644 index 00000000000..355b6d49568 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/CriteriaResourceMatcher.java @@ -0,0 +1,162 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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 ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams; +import ca.uhn.fhir.jpa.dao.MatchUrlService; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.param.BaseParamWithPrefix; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +@Service +public class CriteriaResourceMatcher { + + @Autowired + private FhirContext myContext; + @Autowired + private MatchUrlService myMatchUrlService; + @Autowired + ISearchParamRegistry mySearchParamRegistry; + + public SubscriptionMatchResult match(String theCriteria, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) { + SearchParameterMap searchParameterMap; + try { + searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, theResourceDefinition); + } catch (UnsupportedOperationException e) { + return new SubscriptionMatchResult(theCriteria); + } + searchParameterMap.clean(); + if (searchParameterMap.getLastUpdated() != null) { + return new SubscriptionMatchResult(Constants.PARAM_LASTUPDATED, "Qualifiers not supported"); + } + + for (Map.Entry>> entry : searchParameterMap.entrySet()) { + String theParamName = entry.getKey(); + List> theAndOrParams = entry.getValue(); + SubscriptionMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResourceDefinition, theSearchParams); + if (!result.matched()){ + return result; + } + } + return SubscriptionMatchResult.MATCH; + } + + // This method is modelled from SearchBuilder.searchForIdsWithAndOr() + private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List> theAndOrParams, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) { + if (theAndOrParams.isEmpty()) { + return SubscriptionMatchResult.MATCH; + } + + if (hasQualifiers(theAndOrParams)) { + + return new SubscriptionMatchResult(theParamName, "Qualifiers not supported."); + + } + if (hasPrefixes(theAndOrParams)) { + + return new SubscriptionMatchResult(theParamName, "Prefixes not supported."); + + } + if (hasChain(theAndOrParams)) { + return new SubscriptionMatchResult(theParamName, "Chained references are not supported"); + } + if (theParamName.equals(IAnyResource.SP_RES_ID)) { + + return new SubscriptionMatchResult(theParamName); + + } else if (theParamName.equals(IAnyResource.SP_RES_LANGUAGE)) { + + return new SubscriptionMatchResult(theParamName); + + } else if (theParamName.equals(Constants.PARAM_HAS)) { + + return new SubscriptionMatchResult(theParamName); + + } else if (theParamName.equals(Constants.PARAM_TAG) || theParamName.equals(Constants.PARAM_PROFILE) || theParamName.equals(Constants.PARAM_SECURITY)) { + + return new SubscriptionMatchResult(theParamName); + + } else { + + String resourceName = theResourceDefinition.getName(); + RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName); + return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef); + } + } + + private SubscriptionMatchResult matchResourceParam(String theParamName, List> theAndOrParams, ResourceIndexedSearchParams theSearchParams, String theResourceName, RuntimeSearchParam theParamDef) { + if (theParamDef != null) { + switch (theParamDef.getParamType()) { + case QUANTITY: + case TOKEN: + case STRING: + case NUMBER: + case URI: + case DATE: + case REFERENCE: + return new SubscriptionMatchResult(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theResourceName, theParamName, theParamDef, nextAnd, theSearchParams))); + case COMPOSITE: + case HAS: + case SPECIAL: + default: + return new SubscriptionMatchResult(theParamName); + } + } else { + if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) { + return new SubscriptionMatchResult(theParamName); + } else { + throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName); + } + } + } + + private boolean matchParams(String theResourceName, String theParamName, RuntimeSearchParam paramDef, List theNextAnd, ResourceIndexedSearchParams theSearchParams) { + return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theResourceName, theParamName, paramDef, token)); + } + + private boolean hasChain(List> theAndOrParams) { + return theAndOrParams.stream().flatMap(List::stream).anyMatch(param -> param instanceof ReferenceParam && ((ReferenceParam)param).getChain() != null); + } + + private boolean hasQualifiers(List> theAndOrParams) { + return theAndOrParams.stream().flatMap(List::stream).anyMatch(param -> param.getQueryParameterQualifier() != null); + } + + private boolean hasPrefixes(List> theAndOrParams) { + Predicate hasPrefixPredicate = param -> param instanceof BaseParamWithPrefix && + ((BaseParamWithPrefix) param).getPrefix() != null; + return theAndOrParams.stream().flatMap(List::stream).anyMatch(hasPrefixPredicate); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java index 20abe14988e..9fe70cc1503 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java @@ -23,5 +23,5 @@ package ca.uhn.fhir.jpa.subscription.matcher; import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; public interface ISubscriptionMatcher { - boolean match(String criteria, ResourceModifiedMessage msg); + SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java~HEAD b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java~HEAD new file mode 100644 index 00000000000..22e4943bdad --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java~HEAD @@ -0,0 +1,7 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; + +public interface ISubscriptionMatcher { + boolean match(String criteria, ResourceModifiedMessage msg); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatchResult.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatchResult.java new file mode 100644 index 00000000000..2aad097ebd0 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatchResult.java @@ -0,0 +1,65 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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 SubscriptionMatchResult { + // This could be an enum, but we may want to include details about unsupported matches in the future + public static final SubscriptionMatchResult MATCH = new SubscriptionMatchResult(true); + public static final SubscriptionMatchResult NO_MATCH = new SubscriptionMatchResult(false); + + private final boolean myMatch; + private final boolean mySupported; + private final String myUnsupportedParameter; + private final String myUnsupportedReason; + + public SubscriptionMatchResult(boolean theMatch) { + this.myMatch = theMatch; + this.mySupported = true; + this.myUnsupportedParameter = null; + this.myUnsupportedReason = null; + } + + public SubscriptionMatchResult(String theUnsupportedParameter) { + this.myMatch = false; + this.mySupported = false; + this.myUnsupportedParameter = theUnsupportedParameter; + this.myUnsupportedReason = "Parameter not supported"; + } + + public SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason) { + this.myMatch = false; + this.mySupported = false; + this.myUnsupportedParameter = theUnsupportedParameter; + this.myUnsupportedReason = theUnsupportedReason; + } + + public boolean supported() { + return mySupported; + } + + public boolean matched() { + return myMatch; + } + + public String getUnsupportedReason() { + return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherCompositeInMemoryDatabase.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherCompositeInMemoryDatabase.java new file mode 100644 index 00000000000..788aa9ef139 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherCompositeInMemoryDatabase.java @@ -0,0 +1,55 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class SubscriptionMatcherCompositeInMemoryDatabase implements ISubscriptionMatcher { + private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherCompositeInMemoryDatabase.class); + + @Autowired + SubscriptionMatcherDatabase mySubscriptionMatcherDatabase; + @Autowired + SubscriptionMatcherInMemory mySubscriptionMatcherInMemory; + @Autowired + DaoConfig myDaoConfig; + + @Override + public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) { + SubscriptionMatchResult result; + if (myDaoConfig.isEnableInMemorySubscriptionMatching()) { + result = mySubscriptionMatcherInMemory.match(criteria, msg); + if (!result.supported()) { + ourLog.info("Criteria {} not supported by InMemoryMatcher: {}. Reverting to DatabaseMatcher", criteria, result.getUnsupportedReason()); + result = mySubscriptionMatcherDatabase.match(criteria, msg); + } + } else { + result = mySubscriptionMatcherDatabase.match(criteria, msg); + } + return result; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherDatabase.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherDatabase.java index c4d58963a68..de1a14adfaa 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherDatabase.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherDatabase.java @@ -20,37 +20,39 @@ package ca.uhn.fhir.jpa.subscription.matcher; * #L% */ +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.jpa.dao.DaoRegistry; +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; +import ca.uhn.fhir.jpa.dao.MatchUrlService; +import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; -import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; -import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.api.server.RequestDetails; - +@Service +@Lazy public class SubscriptionMatcherDatabase implements ISubscriptionMatcher { private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherDatabase.class); - private final IFhirResourceDao mySubscriptionDao; - - private final BaseSubscriptionInterceptor mySubscriptionInterceptor; - - public SubscriptionMatcherDatabase(IFhirResourceDao theSubscriptionDao, BaseSubscriptionInterceptor theSubscriptionInterceptor) { - mySubscriptionDao = theSubscriptionDao; - mySubscriptionInterceptor = theSubscriptionInterceptor; - } - + @Autowired + private FhirContext myCtx; + @Autowired + DaoRegistry myDaoRegistry; + @Autowired + MatchUrlService myMatchUrlService; + @Override - public boolean match(String criteria, ResourceModifiedMessage msg) { - IIdType id = msg.getId(getContext()); + public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) { + IIdType id = msg.getId(myCtx); String resourceType = id.getResourceType(); String resourceId = id.getIdPart(); @@ -61,27 +63,23 @@ public class SubscriptionMatcherDatabase implements ISubscriptionMatcher { ourLog.debug("Subscription check found {} results for query: {}", results.size(), criteria); - return results.size() > 0; + return new SubscriptionMatchResult(results.size() > 0); } /** * Search based on a query criteria */ protected IBundleProvider performSearch(String theCriteria) { - RuntimeResourceDefinition responseResourceDef = mySubscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria); - SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(mySubscriptionDao, getContext(), theCriteria, responseResourceDef); + IFhirResourceDao subscriptionDao = myDaoRegistry.getResourceDao("Subscription"); + RuntimeResourceDefinition responseResourceDef = subscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria); + SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef); RequestDetails req = new ServletSubRequestDetails(); req.setSubRequest(true); - IFhirResourceDao responseDao = mySubscriptionInterceptor.getDao(responseResourceDef.getImplementingClass()); + IFhirResourceDao responseDao = myDaoRegistry.getResourceDao(responseResourceDef.getImplementingClass()); responseCriteriaUrl.setLoadSynchronousUpTo(1); - IBundleProvider responseResults = responseDao.search(responseCriteriaUrl, req); - return responseResults; - } - - public FhirContext getContext() { - return mySubscriptionDao.getContext(); + return responseDao.search(responseCriteriaUrl, req); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java index 0d1dcaa8ae0..8351d5c0248 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java @@ -20,13 +20,48 @@ package ca.uhn.fhir.jpa.subscription.matcher; * #L% */ +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams; +import ca.uhn.fhir.jpa.dao.index.SearchParamExtractorService; +import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +@Service +@Lazy public class SubscriptionMatcherInMemory implements ISubscriptionMatcher { + @Autowired + private FhirContext myContext; + @Autowired + private CriteriaResourceMatcher myCriteriaResourceMatcher; + @Autowired + private SearchParamExtractorService mySearchParamExtractorService; + @Override - public boolean match(String criteria, ResourceModifiedMessage msg) { - // FIXME KHS implement - return true; + public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) { + try { + return match(criteria, msg.getNewPayload(myContext)); + } catch (Exception e) { + throw new InternalErrorException("Failure processing resource ID[" + msg.getId(myContext) + "] for subscription ID[" + msg.getSubscriptionId() + "]: " + e.getMessage(), e); + } + } + + SubscriptionMatchResult match(String criteria, IBaseResource resource) { + ResourceTable entity = new ResourceTable(); + String resourceType = myContext.getResourceDefinition(resource).getName(); + entity.setResourceType(resourceType); + ResourceIndexedSearchParams searchParams = new ResourceIndexedSearchParams(); + mySearchParamExtractorService.extractFromResource(searchParams, entity, resource); + mySearchParamExtractorService.extractInlineReferences(resource); + mySearchParamExtractorService.extractResourceLinks(searchParams, entity, resource, resource.getMeta().getLastUpdated(), false); + RuntimeResourceDefinition resourceDefinition = myContext.getResourceDefinition(resource); + return myCriteriaResourceMatcher.match(criteria, resourceDefinition, searchParams); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java index 37ed373598c..dd34f1d5abd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java @@ -21,8 +21,12 @@ package ca.uhn.fhir.jpa.subscription.resthook; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.subscription.*; +import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDeliverySubscriber; +import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; +import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.ResourceDeliveryMessage; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.*; @@ -35,7 +39,9 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Scope; import org.springframework.messaging.MessagingException; +import org.springframework.stereotype.Component; import java.io.IOException; import java.util.ArrayList; @@ -45,11 +51,13 @@ import java.util.Map; import static org.apache.commons.lang3.StringUtils.isNotBlank; +@Component +@Scope("prototype") public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDeliverySubscriber { private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class); - public SubscriptionDeliveringRestHookSubscriber(IFhirResourceDao theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) { - super(theSubscriptionDao, theChannelType, theSubscriptionInterceptor); + public SubscriptionDeliveringRestHookSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) { + super(theChannelType, theSubscriptionInterceptor); } protected void deliverPayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient) { @@ -122,12 +130,14 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe protected IBaseResource getAndMassagePayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription) { IBaseResource payloadResource = theMsg.getPayload(getContext()); - if (theSubscription.getRestHookDetails().isDeliverLatestVersion()) { - IFhirResourceDao dao = getSubscriptionInterceptor().getDao(payloadResource.getClass()); + if (payloadResource == null || theSubscription.getRestHookDetails().isDeliverLatestVersion()) { + IIdType payloadId = theMsg.getPayloadId(getContext()); + RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(payloadId.getResourceType()); + IFhirResourceDao dao = getSubscriptionInterceptor().getDao(resourceDef.getImplementingClass()); try { - payloadResource = dao.read(payloadResource.getIdElement().toVersionless()); + payloadResource = dao.read(payloadId.toVersionless()); } catch (ResourceGoneException e) { - ourLog.warn("Resource {} is deleted, not going to deliver for subscription {}", payloadResource.getIdElement(), theSubscription.getIdElement(getContext())); + ourLog.warn("Resource {} is deleted, not going to deliver for subscription {}", payloadId.toVersionless(), theSubscription.getIdElement(getContext())); return null; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionRestHookInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionRestHookInterceptor.java index 551f9893725..06668501498 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionRestHookInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionRestHookInterceptor.java @@ -22,18 +22,19 @@ package ca.uhn.fhir.jpa.subscription.resthook; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.MessageHandler; import java.util.Optional; public class SubscriptionRestHookInterceptor extends BaseSubscriptionInterceptor { - private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionRestHookInterceptor.class); + @Autowired + BeanFactory myBeanFactory; @Override protected Optional createDeliveryHandler(CanonicalSubscription theSubscription) { - SubscriptionDeliveringRestHookSubscriber value = new SubscriptionDeliveringRestHookSubscriber(getSubscriptionDao(), getChannelType(), this); + SubscriptionDeliveringRestHookSubscriber value = myBeanFactory.getBean(SubscriptionDeliveringRestHookSubscriber.class, getChannelType(), this); return Optional.of(value); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketInterceptor.java index 9189ddc6cc2..9eb56024454 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketInterceptor.java @@ -20,28 +20,15 @@ package ca.uhn.fhir.jpa.subscription.websocket; * #L% */ -import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; -import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; import org.hl7.fhir.r4.model.Subscription; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.MessageHandler; -import org.springframework.transaction.PlatformTransactionManager; import java.util.Optional; public class SubscriptionWebsocketInterceptor extends BaseSubscriptionInterceptor { - @Autowired - private ISubscriptionTableDao mySubscriptionTableDao; - - @Autowired - private PlatformTransactionManager myTxManager; - - @Autowired - private IResourceTableDao myResourceTableDao; - @Override protected Optional createDeliveryHandler(CanonicalSubscription theSubscription) { return Optional.empty(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java index 0dfb31e8985..6007a3318b8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java @@ -863,8 +863,8 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, } if (relCount > 0) { - ourLog.info("Saved {} deferred relationships ({} remain) in {}ms ({}ms / code)", - relCount, myConceptLinksToSaveLater.size(), stopwatch.getMillis(), stopwatch.getMillisPerOperation(codeCount)); + ourLog.info("Saved {} deferred relationships ({} remain) in {}ms ({}ms / entry)", + relCount, myConceptLinksToSaveLater.size(), stopwatch.getMillis(), stopwatch.getMillisPerOperation(relCount)); } if ((myDeferredConcepts.size() + myConceptLinksToSaveLater.size()) == 0) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcDstu3.java index c5e880db83d..306d7854c78 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcDstu3.java @@ -3,7 +3,6 @@ package ca.uhn.fhir.jpa.term; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem; -import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.CoverageIgnore; @@ -21,9 +20,6 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.PersistenceContextType; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -53,12 +49,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class HapiTerminologySvcDstu3 extends BaseHapiTerminologySvcImpl implements IValidationSupport, IHapiTerminologySvcDstu3 { - @PersistenceContext(type = PersistenceContextType.TRANSACTION) - protected EntityManager myEntityManager; - @Autowired - protected FhirContext myContext; - @Autowired - protected ITermCodeSystemDao myCodeSystemDao; @Autowired @Qualifier("myValueSetDaoDstu3") private IFhirResourceDao myValueSetResourceDao; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcR4.java index 8c62fca6ba0..e16551171bd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/HapiTerminologySvcR4.java @@ -2,7 +2,6 @@ package ca.uhn.fhir.jpa.term; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.util.CoverageIgnore; import ca.uhn.fhir.util.UrlUtil; @@ -20,9 +19,6 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.persistence.PersistenceContextType; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -51,10 +47,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public class HapiTerminologySvcR4 extends BaseHapiTerminologySvcImpl implements IHapiTerminologySvcR4 { - @Autowired - protected ITermCodeSystemDao myCodeSystemDao; - @PersistenceContext(type = PersistenceContextType.TRANSACTION) - protected EntityManager myEntityManager; @Autowired @Qualifier("myConceptMapDaoR4") private IFhirResourceDao myConceptMapResourceDao; @@ -68,8 +60,6 @@ public class HapiTerminologySvcR4 extends BaseHapiTerminologySvcImpl implements private IValidationSupport myValidationSupport; @Autowired private IHapiTerminologySvc myTerminologySvc; - @Autowired - private FhirContext myContext; private void addAllChildren(String theSystemString, ConceptDefinitionComponent theCode, List theListToPopulate) { if (isNotBlank(theCode.getCode())) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcImpl.java index 0f647f57b4c..9aa00d00514 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcImpl.java @@ -188,48 +188,50 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { @Override public UploadStatistics loadLoinc(List theFiles, RequestDetails theRequestDetails) { - LoadedFileDescriptors descriptors = new LoadedFileDescriptors(theFiles); - List mandatoryFilenameFragments = Arrays.asList( - LOINC_FILE, - LOINC_HIERARCHY_FILE, - LOINC_UPLOAD_PROPERTIES_FILE, - LOINC_ANSWERLIST_FILE, - LOINC_ANSWERLIST_LINK_FILE, - LOINC_PART_FILE, - LOINC_PART_LINK_FILE, - LOINC_PART_RELATED_CODE_MAPPING_FILE, - LOINC_DOCUMENT_ONTOLOGY_FILE, - LOINC_RSNA_PLAYBOOK_FILE, - LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE, - LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE, - LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE, - LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_CSV, - LOINC_IMAGING_DOCUMENT_CODES_FILE - ); + try (LoadedFileDescriptors descriptors = new LoadedFileDescriptors(theFiles)) { + List mandatoryFilenameFragments = Arrays.asList( + LOINC_FILE, + LOINC_HIERARCHY_FILE, + LOINC_UPLOAD_PROPERTIES_FILE, + LOINC_ANSWERLIST_FILE, + LOINC_ANSWERLIST_LINK_FILE, + LOINC_PART_FILE, + LOINC_PART_LINK_FILE, + LOINC_PART_RELATED_CODE_MAPPING_FILE, + LOINC_DOCUMENT_ONTOLOGY_FILE, + LOINC_RSNA_PLAYBOOK_FILE, + LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE, + LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE, + LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE, + LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_CSV, + LOINC_IMAGING_DOCUMENT_CODES_FILE + ); descriptors.verifyMandatoryFilesExist(mandatoryFilenameFragments); - List optionalFilenameFragments = Arrays.asList( - ); + List optionalFilenameFragments = Arrays.asList( + ); descriptors.verifyOptionalFilesExist(optionalFilenameFragments); ourLog.info("Beginning LOINC processing"); return processLoincFiles(descriptors, theRequestDetails); + } } @Override public UploadStatistics loadSnomedCt(List theFiles, RequestDetails theRequestDetails) { - LoadedFileDescriptors descriptors = new LoadedFileDescriptors(theFiles); + try (LoadedFileDescriptors descriptors = new LoadedFileDescriptors(theFiles)) { - List expectedFilenameFragments = Arrays.asList( - SCT_FILE_DESCRIPTION, - SCT_FILE_RELATIONSHIP, - SCT_FILE_CONCEPT); - descriptors.verifyMandatoryFilesExist(expectedFilenameFragments); + List expectedFilenameFragments = Arrays.asList( + SCT_FILE_DESCRIPTION, + SCT_FILE_RELATIONSHIP, + SCT_FILE_CONCEPT); + descriptors.verifyMandatoryFilesExist(expectedFilenameFragments); - ourLog.info("Beginning SNOMED CT processing"); + ourLog.info("Beginning SNOMED CT processing"); - return processSnomedCtFiles(descriptors, theRequestDetails); + return processSnomedCtFiles(descriptors, theRequestDetails); + } } UploadStatistics processLoincFiles(LoadedFileDescriptors theDescriptors, RequestDetails theRequestDetails) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/ReindexController.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/ReindexController.java new file mode 100644 index 00000000000..abb4a03b87a --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/ReindexController.java @@ -0,0 +1,19 @@ +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * 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% + */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java index a5f281a00ce..32c020743ad 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java @@ -78,6 +78,14 @@ public class TestUtil { for (Field nextField : theClazz.getDeclaredFields()) { ourLog.info(" * Scanning field: {}", nextField.getName()); scan(nextField, theNames, theIsSuperClass); + + Lob lobClass = nextField.getAnnotation(Lob.class); + if (lobClass != null) { + if (nextField.getType().equals(byte[].class) == false) { + //Validate.isTrue(false); + } + } + } if (theClazz.getSuperclass().equals(Object.class)) { @@ -87,8 +95,8 @@ public class TestUtil { scanClass(theNames, theClazz.getSuperclass(), true); } - private static void scan(AnnotatedElement ae, Set theNames, boolean theIsSuperClass) { - Table table = ae.getAnnotation(Table.class); + private static void scan(AnnotatedElement theAnnotatedElement, Set theNames, boolean theIsSuperClass) { + Table table = theAnnotatedElement.getAnnotation(Table.class); if (table != null) { assertNotADuplicateName(table.name(), theNames); for (UniqueConstraint nextConstraint : table.uniqueConstraints()) { @@ -101,28 +109,28 @@ public class TestUtil { } } - JoinColumn joinColumn = ae.getAnnotation(JoinColumn.class); + JoinColumn joinColumn = theAnnotatedElement.getAnnotation(JoinColumn.class); if (joinColumn != null) { assertNotADuplicateName(joinColumn.name(), null); ForeignKey fk = joinColumn.foreignKey(); if (theIsSuperClass) { - Validate.isTrue(isBlank(fk.name()), "Foreign key on " + ae.toString() + " has a name() and should not as it is a superclass"); + Validate.isTrue(isBlank(fk.name()), "Foreign key on " + theAnnotatedElement.toString() + " has a name() and should not as it is a superclass"); } else { Validate.notNull(fk); - Validate.isTrue(isNotBlank(fk.name()), "Foreign key on " + ae.toString() + " has no name()"); + Validate.isTrue(isNotBlank(fk.name()), "Foreign key on " + theAnnotatedElement.toString() + " has no name()"); Validate.isTrue(fk.name().startsWith("FK_")); assertNotADuplicateName(fk.name(), theNames); } } - Column column = ae.getAnnotation(Column.class); + Column column = theAnnotatedElement.getAnnotation(Column.class); if (column != null) { assertNotADuplicateName(column.name(), null); - Validate.isTrue(column.unique() == false, "Should not use unique attribute on column (use named @UniqueConstraint instead) on " + ae.toString()); + Validate.isTrue(column.unique() == false, "Should not use unique attribute on column (use named @UniqueConstraint instead) on " + theAnnotatedElement.toString()); } - GeneratedValue gen = ae.getAnnotation(GeneratedValue.class); - SequenceGenerator sg = ae.getAnnotation(SequenceGenerator.class); + GeneratedValue gen = theAnnotatedElement.getAnnotation(GeneratedValue.class); + SequenceGenerator sg = theAnnotatedElement.getAnnotation(SequenceGenerator.class); Validate.isTrue((gen != null) == (sg != null)); if (gen != null) { assertNotADuplicateName(gen.generator(), theNames); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDaoTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDaoTest.java index 33ca52e89db..b2752457a3d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDaoTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDaoTest.java @@ -1,42 +1,48 @@ package ca.uhn.fhir.jpa.dao; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.jpa.config.TestR4Config; +import ca.uhn.fhir.model.dstu2.composite.PeriodDt; +import ca.uhn.fhir.model.dstu2.resource.Condition; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.primitive.DateTimeDt; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.util.TestUtil; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.PlatformTransactionManager; + import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import org.junit.AfterClass; -import org.junit.Test; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.model.dstu2.composite.PeriodDt; -import ca.uhn.fhir.model.dstu2.resource.Condition; -import ca.uhn.fhir.model.dstu2.resource.Observation; -import ca.uhn.fhir.model.primitive.DateTimeDt; -import ca.uhn.fhir.rest.param.ReferenceParam; -import ca.uhn.fhir.util.TestUtil; -import org.springframework.transaction.PlatformTransactionManager; - +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {TestR4Config.class}) public class BaseHapiFhirDaoTest extends BaseJpaTest { private static FhirContext ourCtx = FhirContext.forDstu2(); + @Autowired + MatchUrlService myMatchUrlService; + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); } - @Test public void testTranslateMatchUrl() { RuntimeResourceDefinition resourceDef = ourCtx.getResourceDefinition(Condition.class); - - IDao dao = mock(IDao.class); - when(dao.getSearchParamByName(any(RuntimeResourceDefinition.class), eq("patient"))).thenReturn(resourceDef.getSearchParam("patient")); - - SearchParameterMap match = BaseHapiFhirDao.translateMatchUrl(dao, ourCtx, "Condition?patient=304&_lastUpdated=>2011-01-01T11:12:21.0000Z", resourceDef); + ISearchParamRegistry searchParamRegistry = mock(ISearchParamRegistry.class); + when(searchParamRegistry.getSearchParamByName(any(RuntimeResourceDefinition.class), eq("patient"))).thenReturn(resourceDef.getSearchParam("patient")); + SearchParameterMap match = myMatchUrlService.translateMatchUrl("Condition?patient=304&_lastUpdated=>2011-01-01T11:12:21.0000Z", resourceDef); assertEquals("2011-01-01T11:12:21.0000Z", match.getLastUpdated().getLowerBound().getValueAsString()); assertEquals(ReferenceParam.class, match.get("patient").get(0).get(0).getClass()); assertEquals("304", ((ReferenceParam)match.get("patient").get(0).get(0)).getIdPart()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java index 49c587bca56..7b9cd916ee8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java @@ -252,6 +252,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { protected ITermConceptMapGroupElementTargetDao myTermConceptMapGroupElementTargetDao; @Autowired private JpaValidationSupportChainDstu3 myJpaValidationSupportChainDstu3; + @Autowired + protected ISearchParamRegistry mySearchParamRegistry; @After() public void afterCleanupDao() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchWithLuceneDisabledTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchWithLuceneDisabledTest.java index 0580a74b70a..9723eede166 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchWithLuceneDisabledTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchWithLuceneDisabledTest.java @@ -150,9 +150,7 @@ public class FhirResourceDaoDstu3SearchWithLuceneDisabledTest extends BaseJpaTes @Before public void beforePurgeDatabase() { - runInTransaction(() -> { - purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); - }); + purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); } @Before diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java index bab9da7fd53..53fdf87f4ae 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java @@ -208,142 +208,6 @@ public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test } - @Test - public void testIndexTransactionWithMatchUrl() { - Patient pt2 = new Patient(); - pt2.setGender(Enumerations.AdministrativeGender.MALE); - pt2.setBirthDateElement(new DateType("2011-01-02")); - IIdType id2 = myPatientDao.create(pt2).getId().toUnqualifiedVersionless(); - - Coverage cov = new Coverage(); - cov.getBeneficiary().setReference(id2.getValue()); - cov.addIdentifier().setSystem("urn:foo:bar").setValue("123"); - IIdType id3 = myCoverageDao.create(cov).getId().toUnqualifiedVersionless(); - - createUniqueIndexCoverageBeneficiary(); - - myResourceReindexingSvc.markAllResourcesForReindexing(); - myResourceReindexingSvc.forceReindexingPass(); - myResourceReindexingSvc.forceReindexingPass(); - - List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); - assertEquals(uniques.toString(), 1, uniques.size()); - assertEquals("Coverage/" + id3.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue()); - assertEquals("Coverage?beneficiary=Patient%2F" + id2.getIdPart() + "&identifier=urn%3Afoo%3Abar%7C123", uniques.get(0).getIndexString()); - - - } - - @Test - public void testIndexTransactionWithMatchUrl2() { - createUniqueIndexCoverageBeneficiary(); - - String input = "{\n" + - " \"resourceType\": \"Bundle\",\n" + - " \"type\": \"transaction\",\n" + - " \"entry\": [\n" + - " {\n" + - " \"fullUrl\": \"urn:uuid:d2a46176-8e15-405d-bbda-baea1a9dc7f3\",\n" + - " \"resource\": {\n" + - " \"resourceType\": \"Patient\",\n" + - " \"identifier\": [\n" + - " {\n" + - " \"use\": \"official\",\n" + - " \"type\": {\n" + - " \"coding\": [\n" + - " {\n" + - " \"system\": \"http://hl7.org/fhir/v2/0203\",\n" + - " \"code\": \"MR\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"system\": \"FOOORG:FOOSITE:patientid:MR:R\",\n" + - " \"value\": \"007811959\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"request\": {\n" + - " \"method\": \"PUT\",\n" + - " \"url\": \"/Patient?identifier=FOOORG%3AFOOSITE%3Apatientid%3AMR%3AR%7C007811959%2CFOOORG%3AFOOSITE%3Apatientid%3AMR%3AB%7C000929990%2CFOOORG%3AFOOSITE%3Apatientid%3API%3APH%7C00589363%2Chttp%3A%2F%2Fhl7.org%2Ffhir%2Fsid%2Fus-ssn%7C657-01-8133\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"fullUrl\": \"urn:uuid:b58ff639-11d1-4dac-942f-abf4f9a625d7\",\n" + - " \"resource\": {\n" + - " \"resourceType\": \"Coverage\",\n" + - " \"identifier\": [\n" + - " {\n" + - " \"system\": \"FOOORG:FOOSITE:coverage:planId\",\n" + - " \"value\": \"0403-010101\"\n" + - " }\n" + - " ],\n" + - " \"beneficiary\": {\n" + - " \"reference\": \"urn:uuid:d2a46176-8e15-405d-bbda-baea1a9dc7f3\"\n" + - " }\n" + - " },\n" + - " \"request\": {\n" + - " \"method\": \"PUT\",\n" + - " \"url\": \"/Coverage?beneficiary=urn%3Auuid%3Ad2a46176-8e15-405d-bbda-baea1a9dc7f3&identifier=FOOORG%3AFOOSITE%3Acoverage%3AplanId%7C0403-010101\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"fullUrl\": \"urn:uuid:13f5da1a-6601-4c1a-82c9-41527be23fa0\",\n" + - " \"resource\": {\n" + - " \"resourceType\": \"Coverage\",\n" + - " \"contained\": [\n" + - " {\n" + - " \"resourceType\": \"RelatedPerson\",\n" + - " \"id\": \"1\",\n" + - " \"name\": [\n" + - " {\n" + - " \"family\": \"SMITH\",\n" + - " \"given\": [\n" + - " \"FAKER\"\n" + - " ]\n" + - " }\n" + - " ]\n" + - " },\n" + - " {\n" + - " \"resourceType\": \"Organization\",\n" + - " \"id\": \"2\",\n" + - " \"name\": \"MEDICAID\"\n" + - " }\n" + - " ],\n" + - " \"identifier\": [\n" + - " {\n" + - " \"system\": \"FOOORG:FOOSITE:coverage:planId\",\n" + - " \"value\": \"0404-010101\"\n" + - " }\n" + - " ],\n" + - " \"policyHolder\": {\n" + - " \"reference\": \"#1\"\n" + - " },\n" + - " \"beneficiary\": {\n" + - " \"reference\": \"urn:uuid:d2a46176-8e15-405d-bbda-baea1a9dc7f3\"\n" + - " },\n" + - " \"payor\": [\n" + - " {\n" + - " \"reference\": \"#2\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"request\": {\n" + - " \"method\": \"PUT\",\n" + - " \"url\": \"/Coverage?beneficiary=urn%3Auuid%3Ad2a46176-8e15-405d-bbda-baea1a9dc7f3&identifier=FOOORG%3AFOOSITE%3Acoverage%3AplanId%7C0404-010101\"\n" + - " }\n" + - " }\n" + - " ]\n" + - "}"; - - Bundle inputBundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, input); - ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(inputBundle)); - mySystemDao.transaction(mySrd, inputBundle); - - inputBundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, input); - mySystemDao.transaction(mySrd, inputBundle); - - } - @Test public void testReplaceOneWithAnother() { createUniqueBirthdateAndGenderSps(); @@ -473,38 +337,6 @@ public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test assertEquals("Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale", uniques.get(0).getIndexString()); } - @Test - public void testUniqueValuesAreIndexed_StringAndReference() { - createUniqueNameAndManagingOrganizationSps(); - - Organization org = new Organization(); - org.setId("Organization/ORG"); - org.setName("ORG"); - myOrganizationDao.update(org); - - Patient pt1 = new Patient(); - pt1.addName() - .setFamily("FAMILY1") - .addGiven("GIVEN1") - .addGiven("GIVEN2") - .addGiven("GIVEN2"); // GIVEN2 happens twice - pt1.setManagingOrganization(new Reference("Organization/ORG")); - IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); - - List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); - Collections.sort(uniques); - - assertEquals(3, uniques.size()); - assertEquals("Patient/" + id1.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue()); - assertEquals("Patient?name=FAMILY1&organization=Organization%2FORG", uniques.get(0).getIndexString()); - - assertEquals("Patient/" + id1.getIdPart(), uniques.get(1).getResource().getIdDt().toUnqualifiedVersionless().getValue()); - assertEquals("Patient?name=GIVEN1&organization=Organization%2FORG", uniques.get(1).getIndexString()); - - assertEquals("Patient/" + id1.getIdPart(), uniques.get(2).getResource().getIdDt().toUnqualifiedVersionless().getValue()); - assertEquals("Patient?name=GIVEN2&organization=Organization%2FORG", uniques.get(2).getIndexString()); - } - @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java index 068acbd9de4..f15f8f6eb65 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java @@ -2,10 +2,7 @@ package ca.uhn.fhir.jpa.dao.dstu3; import static org.junit.Assert.assertEquals; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; @@ -80,6 +77,16 @@ public class SearchParamExtractorDstu3Test { public void requestRefresh() { // nothing } + + @Override + public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) { + return null; + } + + @Override + public Collection getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) { + return null; + } }; SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new DaoConfig(), ourCtx, ourValidationSupport, searchParamRegistry); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java new file mode 100644 index 00000000000..8170d45dcb4 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java @@ -0,0 +1,109 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.*; +import org.junit.AfterClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.Assert.*; + +public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test { + private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4DeleteTest.class); + + @Test + public void testDeleteMarksResourceAndVersionAsDeleted() { + + Patient p = new Patient(); + p.setActive(true); + IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + + myPatientDao.delete(id); + + // Table should be marked as deleted + runInTransaction(()->{ + ResourceTable resourceTable = myResourceTableDao.findById(id.getIdPartAsLong()).get(); + assertNotNull(resourceTable.getDeleted()); + }); + + // Current version should be marked as deleted + runInTransaction(()->{ + ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), 1); + assertNull(resourceTable.getDeleted()); + }); + runInTransaction(()->{ + ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), 2); + assertNotNull(resourceTable.getDeleted()); + }); + + try { + myPatientDao.read(id.toUnqualifiedVersionless()); + fail(); + } catch (ResourceGoneException e) { + // good + } + + myPatientDao.read(id.toUnqualifiedVersionless().withVersion("1")); + + try { + myPatientDao.read(id.toUnqualifiedVersionless().withVersion("2")); + fail(); + } catch (ResourceGoneException e) { + // good + } + + + } + + @Test + public void testResourceIsConsideredDeletedIfOnlyResourceTableEntryIsDeleted() { + + Patient p = new Patient(); + p.setActive(true); + IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + + myPatientDao.delete(id); + + // Table should be marked as deleted + runInTransaction(()->{ + ResourceTable resourceTable = myResourceTableDao.findById(id.getIdPartAsLong()).get(); + assertNotNull(resourceTable.getDeleted()); + }); + + // Mark the current history version as not-deleted even though the actual resource + // table entry is marked deleted + runInTransaction(()->{ + ResourceHistoryTable resourceTable = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), 2); + resourceTable.setDeleted(null); + myResourceHistoryTableDao.save(resourceTable); + }); + + try { + myPatientDao.read(id.toUnqualifiedVersionless()); + fail(); + } catch (ResourceGoneException e) { + // good + } + + myPatientDao.read(id.toUnqualifiedVersionless().withVersion("1")); + + try { + myPatientDao.read(id.toUnqualifiedVersionless().withVersion("2")); + fail(); + } catch (ResourceGoneException e) { + // good + } + + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java index 13bff7eaab2..ba5e30b268e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java @@ -145,6 +145,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test mySearchParameterDao.create(fooSp, mySrd); + assertEquals(1, myResourceReindexingSvc.forceReindexingPass()); assertEquals(1, myResourceReindexingSvc.forceReindexingPass()); assertEquals(0, myResourceReindexingSvc.forceReindexingPass()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java index 96540b89d92..4732e9648e1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; @@ -12,6 +13,7 @@ import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.util.TestUtil; +import com.google.common.collect.Sets; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; @@ -19,6 +21,7 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; @@ -47,12 +50,14 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); myDaoConfig.setUniqueIndexesCheckedBeforeSave(new DaoConfig().isUniqueIndexesCheckedBeforeSave()); myDaoConfig.setSchedulingDisabled(new DaoConfig().isSchedulingDisabled()); + myDaoConfig.setUniqueIndexesEnabled(new DaoConfig().isUniqueIndexesEnabled()); } @Before public void before() { myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); myDaoConfig.setSchedulingDisabled(true); + myDaoConfig.setUniqueIndexesEnabled(true); SearchBuilder.resetLastHandlerMechanismForUnitTest(); } @@ -419,6 +424,10 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { } + @Autowired + private ISearchParamRegistry mySearchParamRegistry; + + @Test public void testDuplicateUniqueValuesAreReIndexed() { myDaoConfig.setSchedulingDisabled(true); @@ -449,6 +458,10 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { createUniqueObservationSubjectDateCode(); + List uniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams("Observation"); + assertEquals(1, uniqueSearchParams.size()); + assertEquals(3, uniqueSearchParams.get(0).getComponents().size()); + myResourceReindexingSvc.markAllResourcesForReindexing(); myResourceReindexingSvc.forceReindexingPass(); myResourceReindexingSvc.forceReindexingPass(); @@ -557,11 +570,16 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { createUniqueIndexCoverageBeneficiary(); - myResourceReindexingSvc.markAllResourcesForReindexing(); - myResourceReindexingSvc.forceReindexingPass(); - myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.markAllResourcesForReindexing("Coverage"); + // The first pass as a low of EPOCH + assertEquals(1, myResourceReindexingSvc.forceReindexingPass()); + // The second pass has a low of Coverage.lastUpdated + assertEquals(1, myResourceReindexingSvc.forceReindexingPass()); + // The third pass has a low of (Coverage.lastUpdated + 1ms) + assertEquals(0, myResourceReindexingSvc.forceReindexingPass()); List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); + ourLog.info("** Uniques: {}", uniques); assertEquals(uniques.toString(), 1, uniques.size()); assertEquals("Coverage/" + id3.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue()); assertEquals("Coverage?beneficiary=Patient%2F" + id2.getIdPart() + "&identifier=urn%3Afoo%3Abar%7C123", uniques.get(0).getIndexString()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java index 4f7378285f9..124527b3ed3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java @@ -36,10 +36,7 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; @@ -531,7 +528,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { ResourceTable entity = new TransactionTemplate(myTxManager).execute(t -> myEntityManager.find(ResourceTable.class, id.getIdPartAsLong())); assertEquals(Long.valueOf(1), entity.getIndexStatus()); - myResourceReindexingSvc.markAllResourcesForReindexing(); + Long jobId = myResourceReindexingSvc.markAllResourcesForReindexing(); myResourceReindexingSvc.forceReindexingPass(); entity = new TransactionTemplate(myTxManager).execute(t -> myEntityManager.find(ResourceTable.class, id.getIdPartAsLong())); @@ -540,6 +537,17 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { // Just make sure this doesn't cause a choke myResourceReindexingSvc.forceReindexingPass(); + /* + * We expect a final reindex count of 3 because there are 2 resources to + * reindex and the final pass uses the most recent time as the low threshold, + * so it indexes the newest resource one more time. It wouldn't be a big deal + * if this ever got fixed so that it ends up with 2 instead of 3. + */ + runInTransaction(()->{ + Optional reindexCount = myResourceReindexJobDao.getReindexCount(jobId); + assertEquals(3, reindexCount.orElseThrow(()->new NullPointerException("No job " + jobId)).intValue()); + }); + // Try making the resource unparseable TransactionTemplate template = new TransactionTemplate(myTxManager); @@ -815,6 +823,81 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { } + + @Test + public void testTransactionUpdatingManuallyDeletedResource() { + + // Create an observation + Observation obs = new Observation(); + obs.addIdentifier().setSystem("urn:system").setValue("foo"); + IIdType obId = myObservationDao.create(obs).getId(); + + // Manually mark it a deleted + runInTransaction(()->{ + myEntityManager.createNativeQuery("UPDATE HFJ_RESOURCE SET RES_DELETED_AT = CURRENT_TIMESTAMP").executeUpdate(); + }); + + runInTransaction(()->{ + ResourceTable obsTable = myResourceTableDao.findById(obId.getIdPartAsLong()).get(); + assertNotNull(obsTable.getDeleted()); + assertEquals(1L, obsTable.getVersion()); + }); + + // Now create a transaction + + obs = new Observation(); + obs.setId(IdType.newRandomUuid()); + obs.addIdentifier().setSystem("urn:system").setValue("foo"); + + DiagnosticReport dr = new DiagnosticReport(); + dr.setId(IdType.newRandomUuid()); + dr.addIdentifier().setSystem("urn:system").setValue("bar"); + dr.addResult().setReference(obs.getId()); + + Bundle bundle = new Bundle(); + bundle.setType(BundleType.TRANSACTION); + bundle.addEntry() + .setResource(obs) + .setFullUrl(obs.getId()) + .getRequest() + .setMethod(HTTPVerb.PUT) + .setUrl("Observation?identifier=urn:system|foo"); + bundle.addEntry() + .setResource(dr) + .setFullUrl(dr.getId()) + .getRequest() + .setMethod(HTTPVerb.PUT) + .setUrl("DiagnosticReport?identifier=urn:system|bar"); + + Bundle resp = mySystemDao.transaction(mySrd, bundle); + assertEquals(2, resp.getEntry().size()); + + BundleEntryComponent respEntry = resp.getEntry().get(0); + assertEquals(Constants.STATUS_HTTP_200_OK + " OK", respEntry.getResponse().getStatus()); + assertThat(respEntry.getResponse().getLocation(), containsString("Observation/" + obId.getIdPart())); + assertThat(respEntry.getResponse().getLocation(), endsWith("/_history/3")); + assertEquals("3", respEntry.getResponse().getEtag()); + + respEntry = resp.getEntry().get(1); + assertEquals(Constants.STATUS_HTTP_201_CREATED + " Created", respEntry.getResponse().getStatus()); + assertThat(respEntry.getResponse().getLocation(), containsString("DiagnosticReport/")); + assertThat(respEntry.getResponse().getLocation(), endsWith("/_history/1")); + IdType drId = new IdType(respEntry.getResponse().getLocation()); + assertEquals("1", respEntry.getResponse().getEtag()); + + runInTransaction(()->{ + ResourceTable obsTable = myResourceTableDao.findById(obId.getIdPartAsLong()).get(); + assertNull(obsTable.getDeleted()); + assertEquals(3L, obsTable.getVersion()); + }); + + runInTransaction(()->{ + DiagnosticReport savedDr = myDiagnosticReportDao.read(drId); + assertEquals(obId.toUnqualifiedVersionless().getValue(), savedDr.getResult().get(0).getReference()); + }); + + } + @Test public void testTransactionCreateInlineMatchUrlWithOneMatchLastUpdated() { Bundle request = new Bundle(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java index e691a1c83cb..f83fda87179 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java @@ -22,10 +22,7 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; @@ -85,6 +82,16 @@ public class SearchParamExtractorR4Test { public void requestRefresh() { // nothing } + + @Override + public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) { + return null; + } + + @Override + public Collection getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) { + return null; + } }; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParameterMapTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParameterMapTest.java new file mode 100644 index 00000000000..3f2f9f57146 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParameterMapTest.java @@ -0,0 +1,28 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.config.TestR4Config; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.rest.param.HasParam; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {TestR4Config.class}) +public class SearchParameterMapTest { + @Autowired + FhirContext myContext; + + @Test + public void toNormalizedQueryStringTest() { + SearchParameterMap params = new SearchParameterMap(); + params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO")); + String criteria = params.toNormalizedQueryString(myContext); + assertEquals(criteria, "?_has:Observation:identifier:urn:system|FOO=urn%3Asystem%7CFOO"); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/StaleSearchDeletingSvcR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/StaleSearchDeletingSvcR4Test.java index b343ba3fbed..2b68a80ceaf 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/StaleSearchDeletingSvcR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/StaleSearchDeletingSvcR4Test.java @@ -32,7 +32,8 @@ public class StaleSearchDeletingSvcR4Test extends BaseResourceProviderR4Test { super.after(); StaleSearchDeletingSvcImpl staleSearchDeletingSvc = AopTestUtils.getTargetObject(myStaleSearchDeletingSvc); staleSearchDeletingSvc.setCutoffSlackForUnitTest(StaleSearchDeletingSvcImpl.DEFAULT_CUTOFF_SLACK); - StaleSearchDeletingSvcImpl.setMaximumResultsToDeleteForUnitTest(10000); + StaleSearchDeletingSvcImpl.setMaximumResultsToDeleteForUnitTest(StaleSearchDeletingSvcImpl.DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_STMT); + StaleSearchDeletingSvcImpl.setMaximumResultsToDeleteInOnePassForUnitTest(StaleSearchDeletingSvcImpl.DEFAULT_MAX_RESULTS_TO_DELETE_IN_ONE_PAS); } @Override @@ -94,6 +95,7 @@ public class StaleSearchDeletingSvcR4Test extends BaseResourceProviderR4Test { @Test public void testDeleteVeryLargeSearch() { StaleSearchDeletingSvcImpl.setMaximumResultsToDeleteForUnitTest(10); + StaleSearchDeletingSvcImpl.setMaximumResultsToDeleteInOnePassForUnitTest(10); runInTransaction(() -> { Search search = new Search(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImplTest.java index 1549aea1b6a..fc98d880af2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImplTest.java @@ -90,21 +90,52 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest { } @Test - public void testMarkJobsPastThresholdAsDeleted() { + public void testReindexPassOnlyReturnsValuesAtLowThreshold() { mockNothingToExpunge(); mockSingleReindexingJob(null); - mockFourResourcesNeedReindexing(); mockFetchFourResources(); + mockFinalResourceNeedsReindexing(); - mySingleJob.setThresholdHigh(DateUtils.addMinutes(new Date(), -1)); + mySingleJob.setThresholdLow(new Date(40 * DateUtils.MILLIS_PER_DAY)); + Date highThreshold = DateUtils.addMinutes(new Date(), -1); + mySingleJob.setThresholdHigh(highThreshold); + // Run the second pass, which should index no resources (meaning it's time to mark as deleted) mySvc.forceReindexingPass(); - - verify(myResourceTableDao, never()).findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(any(), any(), any()); verify(myResourceTableDao, never()).findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(any(), any(), any(), any()); - verify(myReindexJobDao, times(1)).markAsDeletedById(myIdCaptor.capture()); + verify(myReindexJobDao, never()).markAsDeletedById(any()); + verify(myResourceTableDao, times(1)).findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(myPageRequestCaptor.capture(), myLowCaptor.capture(), myHighCaptor.capture()); + assertEquals(new Date(40 * DateUtils.MILLIS_PER_DAY), myLowCaptor.getAllValues().get(0)); + assertEquals(highThreshold, myHighCaptor.getAllValues().get(0)); - assertEquals(123L, myIdCaptor.getValue().longValue()); + // Should mark the low threshold as 1 milli higher than the ne returned item + verify(myReindexJobDao, times(1)).setThresholdLow(eq(123L), eq(new Date((40 * DateUtils.MILLIS_PER_DAY) + 1L))); + } + + @Test + public void testMarkAsDeletedIfNothingIndexed() { + mockNothingToExpunge(); + mockSingleReindexingJob(null); + mockFetchFourResources(); + // Mock resource fetch + List values = Collections.emptyList(); + when(myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(any(), any(), any())).thenReturn(new SliceImpl<>(values)); + + mySingleJob.setThresholdLow(new Date(40 * DateUtils.MILLIS_PER_DAY)); + Date highThreshold = DateUtils.addMinutes(new Date(), -1); + mySingleJob.setThresholdHigh(highThreshold); + + // Run the second pass, which should index no resources (meaning it's time to mark as deleted) + mySvc.forceReindexingPass(); + verify(myResourceTableDao, never()).findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(any(), any(), any(), any()); + verify(myResourceTableDao, times(1)).findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(myPageRequestCaptor.capture(), myLowCaptor.capture(), myHighCaptor.capture()); + assertEquals(new Date(40 * DateUtils.MILLIS_PER_DAY), myLowCaptor.getAllValues().get(0)); + assertEquals(highThreshold, myHighCaptor.getAllValues().get(0)); + + // This time we shouldn't update the threshold + verify(myReindexJobDao, never()).setThresholdLow(any(), any()); + + verify(myReindexJobDao, times(1)).markAsDeletedById(eq(123L)); } @Test @@ -140,6 +171,8 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest { // Make sure we didn't do anything unexpected verify(myReindexJobDao, times(1)).findAll(any(), eq(false)); verify(myReindexJobDao, times(1)).findAll(any(), eq(true)); + verify(myReindexJobDao, times(1)).getReindexCount(any()); + verify(myReindexJobDao, times(1)).setReindexCount(any(), anyInt()); verifyNoMoreInteractions(myReindexJobDao); } @@ -194,6 +227,8 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest { // Make sure we didn't do anything unexpected verify(myReindexJobDao, times(1)).findAll(any(), eq(false)); verify(myReindexJobDao, times(1)).findAll(any(), eq(true)); + verify(myReindexJobDao, times(1)).getReindexCount(any()); + verify(myReindexJobDao, times(1)).setReindexCount(any(), anyInt()); verifyNoMoreInteractions(myReindexJobDao); } @@ -243,7 +278,13 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest { private void mockFourResourcesNeedReindexing() { // Mock resource fetch List values = Arrays.asList(0L, 1L, 2L, 3L); - when(myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(myPageRequestCaptor.capture(), myLowCaptor.capture(), myHighCaptor.capture())).thenReturn(new SliceImpl<>(values)); + when(myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(any(), any(), any())).thenReturn(new SliceImpl<>(values)); + } + + private void mockFinalResourceNeedsReindexing() { + // Mock resource fetch + List values = Arrays.asList(2L); // the second-last one has the highest time + when(myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(any(), any(), any())).thenReturn(new SliceImpl<>(values)); } private void mockSingleReindexingJob(String theResourceType) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java index 9ce8d0a1ef5..a722b4ac937 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.subscription; +import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test; import ca.uhn.fhir.jpa.subscription.email.JavaMailEmailSender; @@ -16,12 +17,12 @@ import ca.uhn.fhir.rest.api.MethodOutcome; import com.icegreen.greenmail.util.GreenMail; import com.icegreen.greenmail.util.GreenMailUtil; import com.icegreen.greenmail.util.ServerSetup; -import com.icegreen.greenmail.util.ServerSetupTest; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.task.AsyncTaskExecutor; import javax.mail.internet.InternetAddress; @@ -31,19 +32,21 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test { private static final Logger ourLog = LoggerFactory.getLogger(EmailSubscriptionDstu2Test.class); private static GreenMail ourTestSmtp; private static int ourListenerPort; - private SubscriptionEmailInterceptor mySubscriber; private List mySubscriptionIds = new ArrayList<>(); + @Autowired + private SubscriptionEmailInterceptor mySubscriber; @Autowired private List> myResourceDaos; @Autowired + @Qualifier(BaseConfig.TASK_EXECUTOR_NAME) private AsyncTaskExecutor myAsyncTaskExecutor; @After @@ -68,7 +71,6 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test { emailSender.setSmtpServerPort(ourListenerPort); emailSender.start(); - mySubscriber = new SubscriptionEmailInterceptor(); mySubscriber.setEmailSender(emailSender); mySubscriber.setResourceDaos(myResourceDaos); mySubscriber.setFhirContext(myFhirCtx); @@ -142,7 +144,7 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test { Observation observation1 = sendObservation(code, "SNOMED-CT"); - waitForSize(2, 60000, new Callable(){ + waitForSize(2, 60000, new Callable() { @Override public Number call() { int length = ourTestSmtp.getReceivedMessages().length; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringDstu3Test.java index bd3dec2c032..e36eace4790 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringDstu3Test.java @@ -10,7 +10,6 @@ import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -23,6 +22,7 @@ import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; +import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; @@ -30,9 +30,7 @@ import java.util.Collections; import java.util.List; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; /** * Test the rest-hook subscriptions @@ -70,12 +68,15 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te ourRestServer.unregisterInterceptor(ourRestHookSubscriptionInterceptor); - ourSubscriptionTriggeringProvider.cancelAll(); - ourSubscriptionTriggeringProvider.setMaxSubmitPerPass(null); + mySubscriptionTriggeringSvc.cancelAll(); + mySubscriptionTriggeringSvc.setMaxSubmitPerPass(null); myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); } + @Autowired + private SubscriptionTriggeringSvcImpl mySubscriptionTriggeringSvc; + @Before public void beforeRegisterRestHookListener() { ourRestServer.registerInterceptor(ourRestHookSubscriptionInterceptor); @@ -196,7 +197,7 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te waitForSize(50, ourUpdatedPatients); beforeReset(); - ourSubscriptionTriggeringProvider.setMaxSubmitPerPass(33); + mySubscriptionTriggeringSvc.setMaxSubmitPerPass(33); Parameters response = ourClient .operation() @@ -252,7 +253,7 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te waitForSize(50, ourUpdatedPatients); beforeReset(); - ourSubscriptionTriggeringProvider.setMaxSubmitPerPass(33); + mySubscriptionTriggeringSvc.setMaxSubmitPerPass(33); Parameters response = ourClient .operation() @@ -315,7 +316,7 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te waitForSize(0, ourUpdatedPatients); beforeReset(); - ourSubscriptionTriggeringProvider.setMaxSubmitPerPass(50); + mySubscriptionTriggeringSvc.setMaxSubmitPerPass(50); Parameters response = ourClient .operation() diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR3.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR3.java new file mode 100644 index 00000000000..16acc8c1804 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR3.java @@ -0,0 +1,486 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test; +import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.codesystems.MedicationRequestCategory; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SubscriptionMatcherInMemoryTestR3 extends BaseResourceProviderDstu3Test { + @Autowired + SubscriptionMatcherInMemory mySubscriptionMatcherInMemory; + + private void assertUnsupported(IBaseResource resource, String criteria) { + assertFalse(mySubscriptionMatcherInMemory.match(criteria, resource).supported()); + } + + private void assertMatched(IBaseResource resource, String criteria) { + SubscriptionMatchResult result = mySubscriptionMatcherInMemory.match(criteria, resource); + ; + assertTrue(result.supported()); + assertTrue(result.matched()); + } + + private void assertNotMatched(IBaseResource resource, String criteria) { + SubscriptionMatchResult result = mySubscriptionMatcherInMemory.match(criteria, resource); + ; + assertTrue(result.supported()); + assertFalse(result.matched()); + } + + /* + The following tests are copied from an e-mail from a site using HAPI FHIR + */ + + @Test + public void testQuestionnaireResponse() { + String criteria = "QuestionnaireResponse?questionnaire=HomeAbsenceHospitalizationRecord,ARIncenterAbsRecord"; + + { + QuestionnaireResponse qr = new QuestionnaireResponse(); + qr.getQuestionnaire().setReference("Questionnaire/HomeAbsenceHospitalizationRecord"); + assertMatched(qr, criteria); + } + { + QuestionnaireResponse qr = new QuestionnaireResponse(); + qr.getQuestionnaire().setReference("Questionnaire/Other"); + assertNotMatched(qr, criteria); + } + { + QuestionnaireResponse qr = new QuestionnaireResponse(); + qr.getQuestionnaire().setDisplay("Questionnaire/HomeAbsenceHospitalizationRecord"); + assertNotMatched(qr, criteria); + } + } + + @Test + public void testCommunicationRequest() { + String criteria = "CommunicationRequest?occurrence==2018-10-17"; + + { + CommunicationRequest cr = new CommunicationRequest(); + cr.setOccurrence(new DateTimeType("2018-10-17")); + assertMatched(cr, criteria); + } + { + CommunicationRequest cr = new CommunicationRequest(); + cr.setOccurrence(new DateTimeType("2018-10-16")); + assertNotMatched(cr, criteria); + } + { + CommunicationRequest cr = new CommunicationRequest(); + cr.setOccurrence(new DateTimeType("2018-10-16")); + assertNotMatched(cr, criteria); + } + } + + @Test + public void testProcedureRequest() { + String criteria = "ProcedureRequest?intent=original-order"; + + { + ProcedureRequest pr = new ProcedureRequest(); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.ORIGINALORDER); + assertMatched(pr, criteria); + } + { + ProcedureRequest pr = new ProcedureRequest(); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.ORDER); + assertNotMatched(pr, criteria); + } + } + + @Test + public void testObservationContextTypeUnsupported() { + String criteria = "Observation?code=17861-6&context.type=IHD"; + { + Observation obs = new Observation(); + obs.getCode().addCoding().setCode("XXX"); + assertNotMatched(obs, criteria); + } + { + Observation obs = new Observation(); + obs.getCode().addCoding().setCode("17861-6"); + assertUnsupported(obs, criteria); + } + } + + // Check that it still fails fast even if the chained parameter is first + @Test + public void testObservationContextTypeUnsupportedReverse() { + String criteria = "Observation?context.type=IHD&code=17861-6"; + { + Observation obs = new Observation(); + obs.getCode().addCoding().setCode("XXX"); + assertNotMatched(obs, criteria); + } + { + Observation obs = new Observation(); + obs.getCode().addCoding().setCode("17861-6"); + assertUnsupported(obs, criteria); + } + } + + @Test + public void medicationRequestOutpatient() { + // Note the date== evaluates to date=eq which is a legacy format supported by hapi fhir + String criteria = "MedicationRequest?intent=instance-order&category=outpatient&date==2018-10-19"; + + { + MedicationRequest mr = new MedicationRequest(); + mr.setIntent(MedicationRequest.MedicationRequestIntent.INSTANCEORDER); + mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode()); + Dosage dosage = new Dosage(); + Timing timing = new Timing(); + timing.getEvent().add(new DateTimeType("2018-10-19")); + dosage.setTiming(timing); + mr.getDosageInstruction().add(dosage); + assertMatched(mr, criteria); + } + + { + MedicationRequest mr = new MedicationRequest(); + mr.setIntent(MedicationRequest.MedicationRequestIntent.INSTANCEORDER); + mr.getCategory().addCoding().setCode(MedicationRequestCategory.INPATIENT.toCode()); + Dosage dosage = new Dosage(); + Timing timing = new Timing(); + timing.getEvent().add(new DateTimeType("2018-10-19")); + dosage.setTiming(timing); + mr.getDosageInstruction().add(dosage); + assertNotMatched(mr, criteria); + } + + { + MedicationRequest mr = new MedicationRequest(); + mr.setIntent(MedicationRequest.MedicationRequestIntent.INSTANCEORDER); + mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode()); + Dosage dosage = new Dosage(); + Timing timing = new Timing(); + timing.getEvent().add(new DateTimeType("2018-10-20")); + dosage.setTiming(timing); + mr.getDosageInstruction().add(dosage); + assertNotMatched(mr, criteria); + } + } + + @Test + public void testMedicationRequestStatuses() { + String criteria = "MedicationRequest?intent=plan&category=outpatient&status=suspended,entered-in-error,cancelled,stopped"; + + // Note suspended is an invalid status and will never match + { + MedicationRequest mr = new MedicationRequest(); + mr.setIntent(MedicationRequest.MedicationRequestIntent.PLAN); + mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode()); + mr.setStatus(MedicationRequest.MedicationRequestStatus.ENTEREDINERROR); + assertMatched(mr, criteria); + } + { + MedicationRequest mr = new MedicationRequest(); + mr.setIntent(MedicationRequest.MedicationRequestIntent.PLAN); + mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode()); + mr.setStatus(MedicationRequest.MedicationRequestStatus.CANCELLED); + assertMatched(mr, criteria); + } + { + MedicationRequest mr = new MedicationRequest(); + mr.setIntent(MedicationRequest.MedicationRequestIntent.PLAN); + mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode()); + mr.setStatus(MedicationRequest.MedicationRequestStatus.STOPPED); + assertMatched(mr, criteria); + } + { + MedicationRequest mr = new MedicationRequest(); + mr.setIntent(MedicationRequest.MedicationRequestIntent.PLAN); + mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode()); + mr.setStatus(MedicationRequest.MedicationRequestStatus.ACTIVE); + assertNotMatched(mr, criteria); + } + } + + @Test + public void testBloodTest() { + String criteria = "Observation?code=FR_Org1Blood2nd,FR_Org1Blood3rd,FR_Org%201BldCult,FR_Org2Blood2nd,FR_Org2Blood3rd,FR_Org%202BldCult,FR_Org3Blood2nd,FR_Org3Blood3rd,FR_Org3BldCult,FR_Org4Blood2nd,FR_Org4Blood3rd,FR_Org4BldCult,FR_Org5Blood2nd,FR_Org5Blood3rd,FR_Org%205BldCult,FR_Org6Blood2nd,FR_Org6Blood3rd,FR_Org6BldCult,FR_Org7Blood2nd,FR_Org7Blood3rd,FR_Org7BldCult,FR_Org8Blood2nd,FR_Org8Blood3rd,FR_Org8BldCult,FR_Org9Blood2nd,FR_Org9Blood3rd,FR_Org9BldCult,FR_Bld2ndCulture,FR_Bld3rdCulture,FR_Blood%20Culture,FR_Com1Bld3rd,FR_Com1BldCult,FR_Com2Bld2nd,FR_Com2Bld3rd,FR_Com2BldCult,FR_CultureBld2nd,FR_CultureBld3rd,FR_CultureBldCul,FR_GmStainBldCul,FR_GramStain2Bld,FR_GramStain3Bld,FR_GramStNegBac&context.type=IHD"; + + { + Observation obs = new Observation(); + obs.getCode().addCoding().setCode("FR_Org1Blood2nd"); + assertUnsupported(obs, criteria); + } + { + Observation obs = new Observation(); + obs.getCode().addCoding().setCode("XXX"); + assertNotMatched(obs, criteria); + } + } + + @Test + public void testProcedureHemodialysis() { + String criteria = "Procedure?category=Hemodialysis"; + + { + Procedure proc = new Procedure(); + proc.getCategory().addCoding().setCode("Hemodialysis"); + assertMatched(proc, criteria); + } + { + Procedure proc = new Procedure(); + proc.getCategory().addCoding().setCode("XXX"); + assertNotMatched(proc, criteria); + } + } + + @Test + public void testProcedureHDStandard() { + String criteria = "Procedure?code=HD_Standard&status=completed&location=Lab123"; + + { + Procedure proc = new Procedure(); + proc.getCode().addCoding().setCode("HD_Standard"); + proc.setStatus(Procedure.ProcedureStatus.COMPLETED); + IIdType locId = new IdType("Location", "Lab123"); + proc.getLocation().setReference(locId.getValue()); + assertMatched(proc, criteria); + } + { + Procedure proc = new Procedure(); + proc.getCode().addCoding().setCode("HD_Standard"); + proc.setStatus(Procedure.ProcedureStatus.COMPLETED); + IIdType locId = new IdType("Location", "XXX"); + proc.getLocation().setReference(locId.getValue()); + assertNotMatched(proc, criteria); + } + { + Procedure proc = new Procedure(); + proc.getCode().addCoding().setCode("XXX"); + proc.setStatus(Procedure.ProcedureStatus.COMPLETED); + IIdType locId = new IdType("Location", "Lab123"); + proc.getLocation().setReference(locId.getValue()); + assertNotMatched(proc, criteria); + } + } + + @Test + public void testProvenance() { + String criteria = "Provenance?activity=http://hl7.org/fhir/v3/DocumentCompletion%7CAU"; + + SearchParameter sp = new SearchParameter(); + sp.addBase("Provenance"); + sp.setCode("activity"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("Provenance.activity"); + sp.setXpathUsage(org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(sp); + mySearchParamRegsitry.forceRefresh(); + + { + Provenance prov = new Provenance(); + prov.setActivity(new Coding().setSystem("http://hl7.org/fhir/v3/DocumentCompletion").setCode("AU")); + assertMatched(prov, criteria); + } + { + Provenance prov = new Provenance(); + assertNotMatched(prov, criteria); + } + { + Provenance prov = new Provenance(); + prov.setActivity(new Coding().setCode("XXX")); + assertNotMatched(prov, criteria); + } + + } + + @Test + public void testBodySite() { + String criteria = "BodySite?accessType=Catheter,PD%20Catheter"; + + SearchParameter sp = new SearchParameter(); + sp.addBase("BodySite"); + sp.setCode("accessType"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("BodySite.extension('BodySite#accessType')"); + sp.setXpathUsage(org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(sp); + mySearchParamRegsitry.forceRefresh(); + + { + BodySite bodySite = new BodySite(); + bodySite.addExtension().setUrl("BodySite#accessType").setValue(new Coding().setCode("Catheter")); + assertMatched(bodySite, criteria); + } + { + BodySite bodySite = new BodySite(); + bodySite.addExtension().setUrl("BodySite#accessType").setValue(new Coding().setCode("PD Catheter")); + assertMatched(bodySite, criteria); + } + { + BodySite bodySite = new BodySite(); + assertNotMatched(bodySite, criteria); + } + { + BodySite bodySite = new BodySite(); + bodySite.addExtension().setUrl("BodySite#accessType").setValue(new Coding().setCode("XXX")); + assertNotMatched(bodySite, criteria); + } + + } + + @Test + public void testProcedureAnyLocation() { + String criteria = "Procedure?code=HD_Standard&status=completed"; + { + Procedure proc = new Procedure(); + proc.getCode().addCoding().setCode("HD_Standard"); + proc.setStatus(Procedure.ProcedureStatus.COMPLETED); + IIdType locId = new IdType("Location", "Lab456"); + proc.getLocation().setReference(locId.getValue()); + assertMatched(proc, criteria); + } + { + Procedure proc = new Procedure(); + proc.getCode().addCoding().setCode("HD_Standard"); + proc.setStatus(Procedure.ProcedureStatus.ABORTED); + assertNotMatched(proc, criteria); + } + { + Procedure proc = new Procedure(); + proc.getCode().addCoding().setCode("XXX"); + proc.setStatus(Procedure.ProcedureStatus.COMPLETED); + assertNotMatched(proc, criteria); + } + } + + @Test + public void testQuestionnaireResponseLong() { + String criteria = "QuestionnaireResponse?questionnaire=HomeAbsenceHospitalizationRecord,ARIncenterAbsRecord,FMCSWDepressionSymptomsScreener,FMCAKIComprehensiveSW,FMCSWIntensiveScreener,FMCESRDComprehensiveSW,FMCNutritionProgressNote,FMCAKIComprehensiveRN"; + + { + QuestionnaireResponse qr = new QuestionnaireResponse(); + qr.getQuestionnaire().setReference("Questionnaire/HomeAbsenceHospitalizationRecord"); + assertMatched(qr, criteria); + } + { + QuestionnaireResponse qr = new QuestionnaireResponse(); + qr.getQuestionnaire().setReference("Questionnaire/FMCSWIntensiveScreener"); + assertMatched(qr, criteria); + } + { + QuestionnaireResponse qr = new QuestionnaireResponse(); + qr.getQuestionnaire().setReference("Questionnaire/FMCAKIComprehensiveRN"); + assertMatched(qr, criteria); + } + { + QuestionnaireResponse qr = new QuestionnaireResponse(); + assertNotMatched(qr, criteria); + } + { + QuestionnaireResponse qr = new QuestionnaireResponse(); + qr.getQuestionnaire().setReference("Questionnaire/FMCAKIComprehensiveRM"); + assertNotMatched(qr, criteria); + } + } + + @Test + public void testProcedureRequestCategory() { + String criteria = "ProcedureRequest?intent=instance-order&category=Laboratory,Ancillary%20Orders,Hemodialysis&occurrence==2018-10-19"; + + SearchParameter sp = new SearchParameter(); + sp.addBase("ProcedureRequest"); + sp.setCode("category"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("ProcedureRequest.category"); + sp.setXpathUsage(org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(sp); + mySearchParamRegsitry.forceRefresh(); + + { + ProcedureRequest pr = new ProcedureRequest(); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER); + CodeableConcept code = new CodeableConcept(); + code.addCoding().setCode("Laboratory"); + pr.getCategory().add(code); + pr.setOccurrence(new DateTimeType("2018-10-19")); + assertMatched(pr, criteria); + } + { + ProcedureRequest pr = new ProcedureRequest(); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER); + CodeableConcept code = new CodeableConcept(); + code.addCoding().setCode("Ancillary Orders"); + pr.getCategory().add(code); + pr.setOccurrence(new DateTimeType("2018-10-19")); + assertMatched(pr, criteria); + } + { + ProcedureRequest pr = new ProcedureRequest(); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER); + CodeableConcept code = new CodeableConcept(); + code.addCoding().setCode("Hemodialysis"); + pr.getCategory().add(code); + pr.setOccurrence(new DateTimeType("2018-10-19")); + assertMatched(pr, criteria); + } + { + ProcedureRequest pr = new ProcedureRequest(); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER); + pr.setOccurrence(new DateTimeType("2018-10-19")); + assertNotMatched(pr, criteria); + } + { + ProcedureRequest pr = new ProcedureRequest(); + CodeableConcept code = new CodeableConcept(); + code.addCoding().setCode("Hemodialysis"); + pr.getCategory().add(code); + pr.setOccurrence(new DateTimeType("2018-10-19")); + assertNotMatched(pr, criteria); + } + { + ProcedureRequest pr = new ProcedureRequest(); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER); + CodeableConcept code = new CodeableConcept(); + code.addCoding().setCode("Hemodialysis"); + pr.getCategory().add(code); + assertNotMatched(pr, criteria); + } + { + ProcedureRequest pr = new ProcedureRequest(); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER); + CodeableConcept code = new CodeableConcept(); + code.addCoding().setCode("XXX"); + pr.getCategory().add(code); + pr.setOccurrence(new DateTimeType("2018-10-19")); + assertNotMatched(pr, criteria); + } + } + + @Test + public void testEposideOfCare() { + String criteria = "EpisodeOfCare?status=active"; + { + EpisodeOfCare eoc = new EpisodeOfCare(); + eoc.setStatus(EpisodeOfCare.EpisodeOfCareStatus.ACTIVE); + assertMatched(eoc, criteria); + } + { + EpisodeOfCare eoc = new EpisodeOfCare(); + assertNotMatched(eoc, criteria); + } + { + EpisodeOfCare eoc = new EpisodeOfCare(); + eoc.setStatus(EpisodeOfCare.EpisodeOfCareStatus.CANCELLED); + assertNotMatched(eoc, criteria); + } + } + + // These last two are covered by other tests above + // String criteria = "ProcedureRequest?intent=original-order&category=Laboratory,Ancillary%20Orders,Hemodialysis&status=suspended,entered-in-error,cancelled"; + // String criteria = "Observation?code=70965-9&context.type=IHD"; +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR4.java new file mode 100644 index 00000000000..9aa08b2cca0 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR4.java @@ -0,0 +1,915 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.config.TestR4Config; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {TestR4Config.class}) +public class SubscriptionMatcherInMemoryTestR4 { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionMatcherInMemoryTestR4.class); + + @Autowired + SubscriptionMatcherInMemory mySubscriptionMatcherInMemory; + @Autowired + FhirContext myContext; + + private SubscriptionMatchResult match(IBaseResource resource, SearchParameterMap params) { + String criteria = params.toNormalizedQueryString(myContext); + ourLog.info("Criteria: <{}>", criteria); + return mySubscriptionMatcherInMemory.match(criteria, resource); + } + + private void assertUnsupported(IBaseResource resource, SearchParameterMap params) { + assertFalse(match(resource, params).supported()); + } + + private void assertMatched(IBaseResource resource, SearchParameterMap params) { + SubscriptionMatchResult result = match(resource, params); + assertTrue(result.getUnsupportedReason(), result.supported()); + assertTrue(result.matched()); + } + + private void assertNotMatched(IBaseResource resource, SearchParameterMap params) { + SubscriptionMatchResult result = match(resource, params); + assertTrue(result.getUnsupportedReason(), result.supported()); + assertFalse(result.matched()); + } + + /* + The following tests are copied from FhirResourceDaoR4SearchNoFtTest + */ + + @Test + public void testChainReferenceUnsupported() { + Encounter enc1 = new Encounter(); + IIdType pid1 = new IdType("Patient", 1L); + enc1.getSubject().setReference(pid1.getValue()); + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "foo|bar").setChain("identifier")); + assertUnsupported(enc1, map); + + MedicationAdministration ma = new MedicationAdministration(); + IIdType mid1 = new IdType("Medication", 1L); + ma.setMedication(new Reference(mid1)); + + map = new SearchParameterMap(); + map.add(MedicationAdministration.SP_MEDICATION, new ReferenceAndListParam().addAnd(new ReferenceOrListParam().add(new ReferenceParam("code", "04823543")))); + assertUnsupported(ma, map); + } + + @Test + public void testHasParameterUnsupported() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester").addGiven("Joe"); + + SearchParameterMap params = new SearchParameterMap(); + params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO")); + String criteria = params.toNormalizedQueryString(myContext); + assertUnsupported(patient, params); + } + + @Test + public void testSearchCode() { + Subscription subs = new Subscription(); + subs.setStatus(Subscription.SubscriptionStatus.ACTIVE); + subs.getChannel().setType(Subscription.SubscriptionChannelType.WEBSOCKET); + subs.setCriteria("Observation?"); + + SearchParameterMap params = new SearchParameterMap(); + assertMatched(subs, params); + + params = new SearchParameterMap(); + params.add(Subscription.SP_TYPE, new TokenParam(null, Subscription.SubscriptionChannelType.WEBSOCKET.toCode())); + params.add(Subscription.SP_STATUS, new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())); + assertMatched(subs, params); + + params = new SearchParameterMap(); + params.add(Subscription.SP_TYPE, new TokenParam(null, Subscription.SubscriptionChannelType.WEBSOCKET.toCode())); + params.add(Subscription.SP_STATUS, new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode() + "2")); + assertNotMatched(subs, params); +// // Wrong param + params = new SearchParameterMap(); + params.add(Subscription.SP_STATUS, new TokenParam(null, Subscription.SubscriptionChannelType.WEBSOCKET.toCode())); + assertNotMatched(subs, params); + } + + @Test + public void testSearchCompositeUnsupported() { + Observation o1 = new Observation(); + o1.getCode().addCoding().setSystem("foo").setCode("testSearchCompositeParamN01"); + o1.setValue(new StringType("testSearchCompositeParamS01")); + + TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamN01"); + StringParam v1 = new StringParam("testSearchCompositeParamS01"); + CompositeParam val = new CompositeParam(v0, v1); + SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_STRING, val); + assertUnsupported(o1, params); + } + + @Test + public void testComponentQuantityWithPrefixUnsupported() { + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1"))) + .setValue(new Quantity().setSystem("http://bar").setCode("code1").setValue(200)); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code2"))) + .setValue(new Quantity().setSystem("http://bar").setCode("code2").setValue(200)); + + String param = Observation.SP_COMPONENT_VALUE_QUANTITY; + QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 150, "http://bar", "code1"); + SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(param, v1); + assertUnsupported(o1, params); + } + + + @Test + public void testComponentQuantityEquals() { + Observation o1 = new Observation(); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1"))) + .setValue(new Quantity().setSystem("http://bar").setCode("code1").setValue(150)); + o1.addComponent() + .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code2"))) + .setValue(new Quantity().setSystem("http://bar").setCode("code2").setValue(150)); + + String param = Observation.SP_COMPONENT_VALUE_QUANTITY; + + QuantityParam v1 = new QuantityParam(null, 150, "http://bar", "code1"); + SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(param, v1); + assertMatched(o1, params); + } + + @Test + public void testIdNotSupported() { + Observation o1 = new Observation(); + SearchParameterMap params = new SearchParameterMap(); + params.add("_id", new StringParam("testSearchForUnknownAlphanumericId")); + assertUnsupported(o1, params); + } + + @Test + public void testLanguageNotSupported() { + Patient patient = new Patient(); + patient.getLanguageElement().setValue("en_CA"); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("testSearchLanguageParam").addGiven("Joe"); + SearchParameterMap params; + params = new SearchParameterMap(); + params.add(IAnyResource.SP_RES_LANGUAGE, new StringParam("en_CA")); + assertUnsupported(patient, params); + } + + @Test + public void testSearchLastUpdatedParamUnsupported() throws InterruptedException { + String methodName = "testSearchLastUpdatedParam"; + DateTimeType today = new DateTimeType(new Date(), TemporalPrecisionEnum.DAY); + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily(methodName).addGiven("Joe"); + SearchParameterMap params = new SearchParameterMap(); + params.setLastUpdated(new DateRangeParam(today, null)); + assertUnsupported(patient, params); + } + + @Test + public void testSearchNameParam() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("testSearchNameParam01Fam").addGiven("testSearchNameParam01Giv"); + + SearchParameterMap params; + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Fam")); + assertMatched(patient, params); + + // Given name shouldn't return for family param + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Giv")); + assertNotMatched(patient, params); + + params = new SearchParameterMap(); + params.add(Patient.SP_NAME, new StringParam("testSearchNameParam01Fam")); + assertMatched(patient, params); + + params = new SearchParameterMap(); + params.add(Patient.SP_NAME, new StringParam("testSearchNameParam01Giv")); + assertMatched(patient, params); + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Foo")); + assertNotMatched(patient, params); + } + + @Test + public void testSearchNumberParam() { + RiskAssessment risk = new RiskAssessment(); + risk.addIdentifier().setSystem("foo").setValue("testSearchNumberParam01"); + risk.addPrediction().setProbability(new DecimalType(2)); + + SearchParameterMap params; + params = new SearchParameterMap().add(RiskAssessment.SP_PROBABILITY, new NumberParam(">1")); + assertUnsupported(risk, params); + + params = new SearchParameterMap().add(RiskAssessment.SP_PROBABILITY, new NumberParam("<1")); + assertUnsupported(risk, params); + + params = new SearchParameterMap().add(RiskAssessment.SP_PROBABILITY, new NumberParam("2")); + assertMatched(risk, params); + + params = new SearchParameterMap().add(RiskAssessment.SP_PROBABILITY, new NumberParam("3")); + assertNotMatched(risk, params); + } + + @Test + public void testSearchNumberWrongParam() { + ImmunizationRecommendation ir1 = new ImmunizationRecommendation(); + ir1.addRecommendation().setDoseNumber(new PositiveIntType(1)); + + SearchParameterMap params = new SearchParameterMap().add(ImmunizationRecommendation.SP_DOSE_NUMBER, new NumberParam("1")); + assertMatched(ir1, params); + params = new SearchParameterMap().add(ImmunizationRecommendation.SP_DOSE_SEQUENCE, new NumberParam("1")); + assertNotMatched(ir1, params); + } + + @Test + public void testSearchPractitionerPhoneAndEmailParam() { + String methodName = "testSearchPractitionerPhoneAndEmailParam"; + Practitioner patient = new Practitioner(); + patient.addName().setFamily(methodName); + patient.addTelecom().setSystem(ContactPoint.ContactPointSystem.PHONE).setValue("123"); + + SearchParameterMap params; + + params = new SearchParameterMap(); + params.add(Practitioner.SP_FAMILY, new StringParam(methodName)); + params.add(Practitioner.SP_EMAIL, new TokenParam(null, "123")); + assertNotMatched(patient, params); + + params = new SearchParameterMap(); + params.add(Practitioner.SP_FAMILY, new StringParam(methodName)); + assertMatched(patient, params); + + params = new SearchParameterMap(); + params.add(Practitioner.SP_FAMILY, new StringParam(methodName)); + params.add(Practitioner.SP_EMAIL, new TokenParam(null, "abc")); + assertNotMatched(patient, params); + + params = new SearchParameterMap(); + params.add(Practitioner.SP_FAMILY, new StringParam(methodName)); + params.add(Practitioner.SP_PHONE, new TokenParam(null, "123")); + assertMatched(patient, params); + } + + @Test + public void testSearchQuantityWrongParam() { + Condition c1 = new Condition(); + c1.setAbatement(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) new SimpleQuantity().setValue(1L))); + SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Condition.SP_ABATEMENT_AGE, new QuantityParam("1")); + assertMatched(c1, params); + + Condition c2 = new Condition(); + c2.setOnset(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) new SimpleQuantity().setValue(1L))); + + params = new SearchParameterMap().add(Condition.SP_ONSET_AGE, new QuantityParam("1")); + assertMatched(c2, params); + } + + @Test + public void testSearchResourceLinkWithChainUnsupported() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChainXX"); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChain01"); + IIdType patientId01 = new IdType("Patient", 1L); + patient.setId(patientId01); + + Patient patient02 = new Patient(); + patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChainXX"); + patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChain02"); + IIdType patientId02 = new IdType("Patient", 2L); + patient02.setId(patientId02); + + Observation obs01 = new Observation(); + obs01.setEffective(new DateTimeType(new Date())); + obs01.setSubject(new Reference(patientId01)); + + Observation obs02 = new Observation(); + obs02.setEffective(new DateTimeType(new Date())); + obs02.setSubject(new Reference(patientId02)); + + SearchParameterMap params = new SearchParameterMap().add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_IDENTIFIER, "urn:system|testSearchResourceLinkWithChain01")); + assertUnsupported(obs01, params); + } + + @Test + public void testSearchResourceLinkWithTextLogicalId() { + Patient patient = new Patient(); + String patientName01 = "testSearchResourceLinkWithTextLogicalId01"; + patient.setId(patientName01); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithTextLogicalIdXX"); + patient.addIdentifier().setSystem("urn:system").setValue(patientName01); + IIdType patientId01 = new IdType("Patient", patientName01); + + Patient patient02 = new Patient(); + String patientName02 = "testSearchResourceLinkWithTextLogicalId02"; + patient02.setId(patientName02); + patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithTextLogicalIdXX"); + patient02.addIdentifier().setSystem("urn:system").setValue(patientName02); + IIdType patientId02 = new IdType("Patient", patientName02); + + Observation obs01 = new Observation(); + obs01.setEffective(new DateTimeType(new Date())); + obs01.setSubject(new Reference(patientId01)); + + Observation obs02 = new Observation(); + obs02.setEffective(new DateTimeType(new Date())); + obs02.setSubject(new Reference(patientId02)); + + SearchParameterMap params = new SearchParameterMap().add(Observation.SP_SUBJECT, new ReferenceParam(patientName01)); + assertMatched(obs01, params); + assertNotMatched(obs02, params); + + params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId99")); + assertNotMatched(obs01, params); + assertNotMatched(obs02, params); + + params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("999999999999999")); + assertNotMatched(obs01, params); + assertNotMatched(obs02, params); + } + + @Test + public void testSearchReferenceInvalid() { + Patient patient = new Patient(); + patient.setId("Patient/123"); + patient.addName().setFamily("FOO"); + patient.getManagingOrganization().setReference("urn:uuid:13720262-b392-465f-913e-54fb198ff954"); + + SearchParameterMap params; + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Fam")); + try { + String criteria = params.toNormalizedQueryString(myContext); + ResourceModifiedMessage msg = new ResourceModifiedMessage(); + msg.setSubscriptionId("Subscription/123"); + msg.setId(new IdType("Patient/ABC")); + msg.setNewPayload(myContext, patient); + SubscriptionMatchResult result = mySubscriptionMatcherInMemory.match(criteria, msg); + fail(); + } catch (InternalErrorException e){ + assertEquals("Failure processing resource ID[Patient/ABC] for subscription ID[Subscription/123]: Invalid resource reference found at path[Patient.managingOrganization] - Does not contain resource type - urn:uuid:13720262-b392-465f-913e-54fb198ff954", e.getMessage()); + } + } + + + @Test + public void testSearchResourceReferenceOnlyCorrectPath() { + Organization org = new Organization(); + org.setActive(true); + IIdType oid1 = new IdType("Organization", 1L); + + Task task = new Task(); + task.setRequester(new Reference(oid1)); + Task task2 = new Task(); + task2.setOwner(new Reference(oid1)); + + SearchParameterMap map; + + map = new SearchParameterMap(); + map.add(Task.SP_REQUESTER, new ReferenceParam(oid1.getValue())); + assertMatched(task, map); + assertNotMatched(task2, map); + } + + @Test + public void testSearchStringParam() throws Exception { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe"); + + SearchParameterMap params; + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_testSearchStringParam")); + assertMatched(patient, params); + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("FOO_testSearchStringParam")); + assertNotMatched(patient, params); + + // Try with different casing + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("tester_testsearchstringparam")); + assertMatched(patient, params); + + params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("TESTER_TESTSEARCHSTRINGPARAM")); + assertMatched(patient, params); + } + + @Test + public void testSearchStringParamReallyLong() { + String methodName = "testSearchStringParamReallyLong"; + String value = StringUtils.rightPad(methodName, 200, 'a'); + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily(value); + + SearchParameterMap params; + + params = new SearchParameterMap(); + + String substring = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); + params.add(Patient.SP_FAMILY, new StringParam(substring)); + assertMatched(patient, params); + } + + @Test + public void testSearchStringParamWithNonNormalized() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().addGiven("testSearchStringParamWithNonNormalized_h\u00F6ra"); + Patient patient2 = new Patient(); + patient2.addIdentifier().setSystem("urn:system").setValue("002"); + patient2.addName().addGiven("testSearchStringParamWithNonNormalized_HORA"); + + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_GIVEN, new StringParam("testSearchStringParamWithNonNormalized_hora")); + assertMatched(patient, params); + assertMatched(patient2, params); + } + + @Test + public void testSearchTokenParam() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001"); + patient.addName().setFamily("Tester").addGiven("testSearchTokenParam1"); + patient.addCommunication().getLanguage().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem") + .setDisplay("testSearchTokenParamDisplay"); + + Patient patient2 = new Patient(); + patient2.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam002"); + patient2.addName().setFamily("Tester").addGiven("testSearchTokenParam2"); + + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", "testSearchTokenParam001")); + assertMatched(patient, map); + assertNotMatched(patient2, map); + } + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new TokenParam(null, "testSearchTokenParam001")); + assertMatched(patient, map); + assertNotMatched(patient2, map); + } + + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_LANGUAGE, new TokenParam("testSearchTokenParamSystem", "testSearchTokenParamCode")); + assertMatched(patient, map); + assertNotMatched(patient2, map); + } + + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_LANGUAGE, new TokenParam(null, "testSearchTokenParamCode", true)); + assertUnsupported(patient, map); + } + + + { + SearchParameterMap map = new SearchParameterMap(); + TokenOrListParam listParam = new TokenOrListParam(); + listParam.add("urn:system", "testSearchTokenParam001"); + listParam.add("urn:system", "testSearchTokenParam002"); + map.add(Patient.SP_IDENTIFIER, listParam); + assertMatched(patient, map); + assertMatched(patient2, map); + } + + { + SearchParameterMap map = new SearchParameterMap(); + TokenOrListParam listParam = new TokenOrListParam(); + listParam.add(null, "testSearchTokenParam001"); + listParam.add("urn:system", "testSearchTokenParam002"); + map.add(Patient.SP_IDENTIFIER, listParam); + assertMatched(patient, map); + assertMatched(patient2, map); + } + } + + @Test + public void testSearchTokenParamNoValue() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001"); + patient.addName().setFamily("Tester").addGiven("testSearchTokenParam1"); + patient.addCommunication().getLanguage().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem") + .setDisplay("testSearchTokenParamDisplay"); + + Patient patient2 = new Patient(); + patient2.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam002"); + patient2.addName().setFamily("Tester").addGiven("testSearchTokenParam2"); + + Patient patient3 = new Patient(); + patient3.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002"); + patient3.addName().setFamily("Tester").addGiven("testSearchTokenParam2"); + + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", null)); + // Match 2 + assertMatched(patient, map); + assertMatched(patient2, map); + assertNotMatched(patient3, map); + } + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", "")); + // Match 2 + assertMatched(patient, map); + assertMatched(patient2, map); + assertNotMatched(patient3, map); + } + } + + @Test + public void testSearchTokenWithNotModifierUnsupported() { + String male, female; + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester").addGiven("Joe"); + patient.setGender(Enumerations.AdministrativeGender.MALE); + + List patients; + SearchParameterMap params; + + params = new SearchParameterMap(); + params.add(Patient.SP_GENDER, new TokenParam(null, "male")); + assertMatched(patient, params); + + params = new SearchParameterMap(); + params.add(Patient.SP_GENDER, new TokenParam(null, "male").setModifier(TokenParamModifier.NOT)); + assertUnsupported(patient, params); + } + + @Test + public void testSearchTokenWrongParam() { + Patient p1 = new Patient(); + p1.setGender(Enumerations.AdministrativeGender.MALE); + + Patient p2 = new Patient(); + p2.addIdentifier().setValue(Enumerations.AdministrativeGender.MALE.toCode()); + + { + SearchParameterMap map = new SearchParameterMap().add(Patient.SP_GENDER, new TokenParam(null, "male")); + assertMatched(p1, map); + assertNotMatched(p2, map); + } + { + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_IDENTIFIER, new TokenParam(null, "male")); + assertNotMatched(p1, map); + } + } + + @Test + public void testSearchUriWrongParam() { + ValueSet v1 = new ValueSet(); + v1.getUrlElement().setValue("http://foo"); + + ValueSet v2 = new ValueSet(); + v2.getExpansion().getIdentifierElement().setValue("http://foo"); + + { + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://foo")); + assertMatched(v1, map); + assertNotMatched(v2, map); + } + { + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_EXPANSION, new UriParam("http://foo")); + assertNotMatched(v1, map); + assertMatched(v2, map); + } + } + + @Test + public void testSearchValueQuantity() { + String methodName = "testSearchValueQuantity"; + + Observation o1 = new Observation(); + o1.getCode().addCoding().setSystem("urn:foo").setCode(methodName + "code"); + Quantity q1 = new Quantity().setSystem("urn:bar:" + methodName).setCode(methodName + "units").setValue(10); + o1.setValue(q1); + Observation o2 = new Observation(); + o2.getCode().addCoding().setSystem("urn:foo").setCode(methodName + "code"); + Quantity q2 = new Quantity().setSystem("urn:bar:" + methodName).setCode(methodName + "units").setValue(5); + o2.setValue(q2); + + SearchParameterMap map; + IBundleProvider found; + QuantityParam param; + + map = new SearchParameterMap(); + param = new QuantityParam(null, new BigDecimal("10"), null, null); + map.add(Observation.SP_VALUE_QUANTITY, param); + assertMatched(o1, map); + assertNotMatched(o2, map); + + map = new SearchParameterMap(); + param = new QuantityParam(null, new BigDecimal("10"), null, methodName + "units"); + map.add(Observation.SP_VALUE_QUANTITY, param); + assertMatched(o1, map); + assertNotMatched(o2, map); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + param = new QuantityParam(null, new BigDecimal("10"), "urn:bar:" + methodName, null); + map.add(Observation.SP_VALUE_QUANTITY, param); + assertMatched(o1, map); + assertNotMatched(o2, map); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + param = new QuantityParam(null, new BigDecimal("10"), "urn:bar:" + methodName, methodName + "units"); + map.add(Observation.SP_VALUE_QUANTITY, param); + assertMatched(o1, map); + assertNotMatched(o2, map); + + map = new SearchParameterMap(); + map.setLoadSynchronous(true); + param = new QuantityParam(null, new BigDecimal("1000"), "urn:bar:" + methodName, methodName + "units"); + map.add(Observation.SP_VALUE_QUANTITY, param); + assertNotMatched(o1, map); + assertNotMatched(o2, map); + } + + @Test + public void testSearchWithContainsUnsupported() { + Patient pt1 = new Patient(); + pt1.addName().setFamily("ABCDEFGHIJK"); + + List ids; + SearchParameterMap map; + IBundleProvider results; + + // Contains = true + map = new SearchParameterMap(); + map.add(Patient.SP_NAME, new StringParam("FGHIJK").setContains(true)); + assertUnsupported(pt1, map); + } + + @Test + public void testSearchWithDate() { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + + Patient patient2 = new Patient(); + patient2.addIdentifier().setSystem("urn:system").setValue("002"); + patient2.addName().setFamily("Tester_testSearchStringParam").addGiven("John"); + patient2.setBirthDateElement(new DateType("2011-01-01")); + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_BIRTHDATE, new DateParam("2011-01-01")); + assertNotMatched(patient, params); + assertMatched(patient2, params); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add(Patient.SP_BIRTHDATE, new DateParam("2011-01-03")); + assertNotMatched(patient, params); + assertNotMatched(patient2, params); + } + } + + @Test + public void testSearchWithIncludesIgnored() { + String methodName = "testSearchWithIncludes"; + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("001"); + patient.addName().setFamily("Tester_" + methodName + "_P1").addGiven("Joe"); + + { + // No includes + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + assertMatched(patient, params); + } + { + // Named include + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(Patient.INCLUDE_ORGANIZATION.asNonRecursive()); + assertMatched(patient, params); + } + { + // Named include with parent non-recursive + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(Patient.INCLUDE_ORGANIZATION); + params.addInclude(Organization.INCLUDE_PARTOF.asNonRecursive()); + assertMatched(patient, params); + } + { + // Named include with parent recursive + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(Patient.INCLUDE_ORGANIZATION); + params.addInclude(Organization.INCLUDE_PARTOF.asRecursive()); + assertMatched(patient, params); + } + { + // * include non recursive + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(IBaseResource.INCLUDE_ALL.asNonRecursive()); + assertMatched(patient, params); + } + { + // * include recursive + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(IBaseResource.INCLUDE_ALL.asRecursive()); + assertMatched(patient, params); + } + { + // Irrelevant include + SearchParameterMap params = new SearchParameterMap(); + params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1")); + params.addInclude(Encounter.INCLUDE_EPISODE_OF_CARE); + assertMatched(patient, params); + } + } + + @Test + public void testSearchWithSecurityAndProfileParamsUnsupported() { + String methodName = "testSearchWithSecurityAndProfileParams"; + + Organization org = new Organization(); + org.getNameElement().setValue("FOO"); + org.getMeta().addSecurity("urn:taglist", methodName + "1a", null); + { + SearchParameterMap params = new SearchParameterMap(); + params.add("_security", new TokenParam("urn:taglist", methodName + "1a")); + assertUnsupported(org, params); + } + { + SearchParameterMap params = new SearchParameterMap(); + params.add("_profile", new UriParam("http://" + methodName)); + assertUnsupported(org, params); + } + } + + @Test + public void testSearchWithTagParameterUnsupported() { + String methodName = "testSearchWithTagParameter"; + + Organization org = new Organization(); + org.getNameElement().setValue("FOO"); + org.getMeta().addTag("urn:taglist", methodName + "1a", null); + org.getMeta().addTag("urn:taglist", methodName + "1b", null); + + { + // One tag + SearchParameterMap params = new SearchParameterMap(); + params.add("_tag", new TokenParam("urn:taglist", methodName + "1a")); + assertUnsupported(org, params); + } + } + + @Test + public void testSearchWithVeryLongUrlLonger() { + Patient p = new Patient(); + p.addName().setFamily("A1"); + + + SearchParameterMap map = new SearchParameterMap(); + StringOrListParam or = new StringOrListParam(); + or.addOr(new StringParam("A1")); + for (int i = 0; i < 50; i++) { + or.addOr(new StringParam(StringUtils.leftPad("", 200, (char) ('A' + i)))); + } + map.add(Patient.SP_NAME, or); + assertMatched(p, map); + + map = new SearchParameterMap(); + or = new StringOrListParam(); + or.addOr(new StringParam("A1")); + or.addOr(new StringParam("A1")); + for (int i = 0; i < 50; i++) { + or.addOr(new StringParam(StringUtils.leftPad("", 200, (char) ('A' + i)))); + } + map.add(Patient.SP_NAME, or); + assertMatched(p, map); + } + + @Test + public void testDateSearchParametersShouldBeTimezoneIndependent() { + + List nlist = new ArrayList<>(); + nlist.add(createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30")); + nlist.add(createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00")); + + List ylist = new ArrayList<>(); + ylist.add(createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30")); + ylist.add(createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00")); + ylist.add(createObservationWithEffective("YES03", "2011-01-02T00:00:00-09:00")); + ylist.add(createObservationWithEffective("YES04", "2011-01-02T00:00:00-08:00")); + ylist.add(createObservationWithEffective("YES05", "2011-01-02T00:00:00-07:00")); + ylist.add(createObservationWithEffective("YES06", "2011-01-02T00:00:00-06:00")); + ylist.add(createObservationWithEffective("YES07", "2011-01-02T00:00:00-05:00")); + ylist.add(createObservationWithEffective("YES08", "2011-01-02T00:00:00-04:00")); + ylist.add(createObservationWithEffective("YES09", "2011-01-02T00:00:00-03:00")); + ylist.add(createObservationWithEffective("YES10", "2011-01-02T00:00:00-02:00")); + ylist.add(createObservationWithEffective("YES11", "2011-01-02T00:00:00-01:00")); + ylist.add(createObservationWithEffective("YES12", "2011-01-02T00:00:00Z")); + ylist.add(createObservationWithEffective("YES13", "2011-01-02T00:00:00+01:00")); + ylist.add(createObservationWithEffective("YES14", "2011-01-02T00:00:00+02:00")); + ylist.add(createObservationWithEffective("YES15", "2011-01-02T00:00:00+03:00")); + ylist.add(createObservationWithEffective("YES16", "2011-01-02T00:00:00+04:00")); + ylist.add(createObservationWithEffective("YES17", "2011-01-02T00:00:00+05:00")); + ylist.add(createObservationWithEffective("YES18", "2011-01-02T00:00:00+06:00")); + ylist.add(createObservationWithEffective("YES19", "2011-01-02T00:00:00+07:00")); + ylist.add(createObservationWithEffective("YES20", "2011-01-02T00:00:00+08:00")); + ylist.add(createObservationWithEffective("YES21", "2011-01-02T00:00:00+09:00")); + ylist.add(createObservationWithEffective("YES22", "2011-01-02T00:00:00+10:00")); + ylist.add(createObservationWithEffective("YES23", "2011-01-02T00:00:00+11:00")); + + + SearchParameterMap map = new SearchParameterMap(); + map.add(Observation.SP_DATE, new DateParam("2011-01-02")); + + for (Observation obs : nlist) { +// assertNotMatched(obs, map); + } + for (Observation obs : ylist) { + ourLog.info("Obs {} has time {}", obs.getId(), obs.getEffectiveDateTimeType().getValue().toString()); + assertMatched(obs, map); + } + } + + private Observation createObservationWithEffective(String theId, String theEffective) { + Observation obs = new Observation(); + obs.setId(theId); + obs.setEffective(new DateTimeType(theEffective)); + return obs; + } + + @Test + public void testSearchWithVeryLongUrlShorter() { + Patient p = new Patient(); + p.addName().setFamily("A1"); + + SearchParameterMap map = new SearchParameterMap(); + StringOrListParam or = new StringOrListParam(); + or.addOr(new StringParam("A1")); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'A'))); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'B'))); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'C'))); + map.add(Patient.SP_NAME, or); + + assertMatched(p, map); + + map = new SearchParameterMap(); + or = new StringOrListParam(); + or.addOr(new StringParam("A1")); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'A'))); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'B'))); + or.addOr(new StringParam(StringUtils.leftPad("", 200, 'C'))); + map.add(Patient.SP_NAME, or); + assertMatched(p, map); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java index 61208c82b17..0c8a07ec59b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.subscription.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test; +import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; import ca.uhn.fhir.jpa.subscription.RestHookTestDstu2Test; import ca.uhn.fhir.jpa.util.JpaConstants; import ca.uhn.fhir.rest.annotation.Create; @@ -13,9 +14,12 @@ import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.PortUtil; import com.google.common.collect.Lists; +import net.ttddyy.dsproxy.QueryCount; +import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; @@ -23,8 +27,10 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; import org.junit.*; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.support.ExecutorSubscribableChannel; +import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collections; @@ -33,8 +39,7 @@ import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; /** * Test the rest-hook subscriptions @@ -50,11 +55,30 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { private static List ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); private static List ourContentTypes = Collections.synchronizedList(new ArrayList<>()); private static List ourHeaders = Collections.synchronizedList(new ArrayList<>()); + private static SingleQueryCountHolder ourCountHolder; private List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); + + @Autowired + private SingleQueryCountHolder myCountHolder; + @Autowired + private DaoConfig myDaoConfig; + private CountingInterceptor myCountingInterceptor; + @PostConstruct + public void initializeOurCountHolder() { + ourCountHolder = myCountHolder; + } + + @Before + public void enableInMemory() { + myDaoConfig.setEnableInMemorySubscriptionMatching(true); + } + @After public void afterUnregisterRestHookListener() { + BaseSubscriptionInterceptor.setForcePayloadEncodeAndDecodeForUnitTests(false); + for (IIdType next : mySubscriptionIds) { IIdType nextId = next.toUnqualifiedVersionless(); ourLog.info("Deleting: {}", nextId); @@ -98,6 +122,16 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { } private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException { + Subscription subscription = newSubscription(theCriteria, thePayload, theEndpoint); + + MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute(); + subscription.setId(methodOutcome.getId().getIdPart()); + mySubscriptionIds.add(methodOutcome.getId()); + + return subscription; + } + + private Subscription newSubscription(String theCriteria, String thePayload, String theEndpoint) { Subscription subscription = new Subscription(); subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED); @@ -107,11 +141,6 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); channel.setPayload(thePayload); channel.setEndpoint(theEndpoint); - - MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute(); - subscription.setId(methodOutcome.getId().getIdPart()); - mySubscriptionIds.add(methodOutcome.getId()); - return subscription; } @@ -308,9 +337,9 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { waitForSize(0, ourCreatedObservations); waitForSize(5, ourUpdatedObservations); - Assert.assertFalse(subscription1.getId().equals(subscription2.getId())); - Assert.assertFalse(observation1.getId().isEmpty()); - Assert.assertFalse(observation2.getId().isEmpty()); + assertFalse(subscription1.getId().equals(subscription2.getId())); + assertFalse(observation1.getId().isEmpty()); + assertFalse(observation2.getId().isEmpty()); } @Test @@ -382,9 +411,62 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { waitForSize(0, ourCreatedObservations); waitForSize(5, ourUpdatedObservations); - Assert.assertFalse(subscription1.getId().equals(subscription2.getId())); - Assert.assertFalse(observation1.getId().isEmpty()); - Assert.assertFalse(observation2.getId().isEmpty()); + assertFalse(subscription1.getId().equals(subscription2.getId())); + assertFalse(observation1.getId().isEmpty()); + assertFalse(observation2.getId().isEmpty()); + } + + @Test + public void testSubscriptionTriggerViaSubscription() throws Exception { + BaseSubscriptionInterceptor.setForcePayloadEncodeAndDecodeForUnitTests(true); + + String payload = "application/xml"; + + String code = "1000000050"; + String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; + + createSubscription(criteria1, payload, ourListenerServerBase); + waitForRegisteredSubscriptionCount(1); + + ourLog.info("** About to send obervation"); + + Observation observation = new Observation(); + observation.addIdentifier().setSystem("foo").setValue("bar1"); + observation.setId(IdType.newRandomUuid().getValue()); + CodeableConcept codeableConcept = new CodeableConcept() + .addCoding(new Coding().setCode(code).setSystem("SNOMED-CT")); + observation.setCode(codeableConcept); + observation.setStatus(Observation.ObservationStatus.FINAL); + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("foo").setValue("bar2"); + patient.setId(IdType.newRandomUuid().getValue()); + patient.setActive(true); + observation.getSubject().setReference(patient.getId()); + + Bundle requestBundle = new Bundle(); + requestBundle.setType(Bundle.BundleType.TRANSACTION); + requestBundle.addEntry() + .setResource(observation) + .setFullUrl(observation.getId()) + .getRequest() + .setUrl("Obervation?identifier=foo|bar1") + .setMethod(Bundle.HTTPVerb.PUT); + requestBundle.addEntry() + .setResource(patient) + .setFullUrl(patient.getId()) + .getRequest() + .setUrl("Patient?identifier=foo|bar2") + .setMethod(Bundle.HTTPVerb.PUT); + ourClient.transaction().withBundle(requestBundle).execute(); + + // Should see 1 subscription notification + waitForSize(0, ourCreatedObservations); + waitForSize(1, ourUpdatedObservations); + assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); + + Observation obs = ourUpdatedObservations.get(0); + ourLog.info("Observation content: {}", myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(obs)); } @Test @@ -533,6 +615,30 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { RestHookTestDstu2Test.waitForQueueToDrain(getRestHookSubscriptionInterceptor()); } + @Test(expected = UnprocessableEntityException.class) + public void testInvalidProvenanceParam() { + String payload = "application/fhir+json"; + String criteriabad = "Provenance?activity=http://hl7.org/fhir/v3/DocumentCompletion%7CAU"; + Subscription subscription = newSubscription(criteriabad, payload, ourListenerServerBase); + ourClient.create().resource(subscription).execute(); + } + + @Test(expected = UnprocessableEntityException.class) + public void testInvalidProcedureRequestParam() { + String payload = "application/fhir+json"; + String criteriabad = "ProcedureRequest?intent=instance-order&category=Laboratory"; + Subscription subscription = newSubscription(criteriabad, payload, ourListenerServerBase); + ourClient.create().resource(subscription).execute(); + } + + @Test(expected = UnprocessableEntityException.class) + public void testInvalidBodySiteParam() { + String payload = "application/fhir+json"; + String criteriabad = "BodySite?accessType=Catheter"; + Subscription subscription = newSubscription(criteriabad, payload, ourListenerServerBase); + ourClient.create().resource(subscription).execute(); + } + public static class ObservationListener implements IResourceProvider { @Create @@ -572,6 +678,15 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { } + @AfterClass + public static void reportTotalSelects() { + ourLog.info("Total database select queries: {}", getQueryCount().getSelect()); + } + + private static QueryCount getQueryCount() { + return ourCountHolder.getQueryCountMap().get(""); + } + @BeforeClass public static void startListenerServer() throws Exception { ourListenerPort = PortUtil.findFreePort(); diff --git a/hapi-fhir-jpaserver-elasticsearch/pom.xml b/hapi-fhir-jpaserver-elasticsearch/pom.xml index 6559439566d..dfee9dca768 100644 --- a/hapi-fhir-jpaserver-elasticsearch/pom.xml +++ b/hapi-fhir-jpaserver-elasticsearch/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-example/README.md b/hapi-fhir-jpaserver-example/README.md index a3176abd3d1..8339170e048 100644 --- a/hapi-fhir-jpaserver-example/README.md +++ b/hapi-fhir-jpaserver-example/README.md @@ -28,7 +28,7 @@ Run the configuration. - Select your server, and click the green triangle (or the bug if you want to debug) - Wait for the console output to stop -Point your browser (or fiddler, or what have you) to `http://localhost:8080/hapi/base/Patient` +Point your browser (or fiddler, or what have you) to `http://localhost:8080/hapi/baseDstu3/Patient` You should get an empty bundle back. @@ -42,6 +42,9 @@ Use this command to start the container: Note: with this command data is persisted across container restarts, but not after removal of the container. Use a docker volume mapping on /var/lib/jetty/target to achieve this. +After the docker container initial startup, point your browser (or fiddler, or what have you) to `http://localhost:8080/baseDstu3/Patient` + +You should get an empty bundle back. #### Using ElasticSearch as the search engine instead of the default Apache Lucene 1. Install ElasticSearch server and the phonetic plugin * Download ElasticSearch from https://www.elastic.co/downloads/elasticsearch diff --git a/hapi-fhir-jpaserver-example/pom.xml b/hapi-fhir-jpaserver-example/pom.xml index 708ffffaee5..1663815eae0 100644 --- a/hapi-fhir-jpaserver-example/pom.xml +++ b/hapi-fhir-jpaserver-example/pom.xml @@ -10,7 +10,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index 0bf2e003448..5a2e5fad948 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -159,12 +159,6 @@ public class JpaServerDemo extends RestfulServer { if (fhirVersion == FhirVersionEnum.DSTU3) { registerProvider(myAppCtx.getBean(TerminologyUploaderProviderDstu3.class)); } - - // Enable various subscription types - registerInterceptor(myAppCtx.getBean(SubscriptionWebsocketInterceptor.class)); - registerInterceptor(myAppCtx.getBean(SubscriptionRestHookInterceptor.class)); - registerInterceptor(myAppCtx.getBean(SubscriptionEmailInterceptor.class)); - } } diff --git a/hapi-fhir-jpaserver-migrate/pom.xml b/hapi-fhir-jpaserver-migrate/pom.xml index 107ff98012a..59f6c03486d 100644 --- a/hapi-fhir-jpaserver-migrate/pom.xml +++ b/hapi-fhir-jpaserver-migrate/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java index 0b800dbb344..514f2fc959f 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java @@ -50,7 +50,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet indexes = metadata.getIndexInfo(null, null, theTableName, false, true); + ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), theTableName, false, true); Set indexNames = new HashSet<>(); while (indexes.next()) { @@ -78,7 +78,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet indexes = metadata.getIndexInfo(null, null, theTableName, false, false); + ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), theTableName, false, false); while (indexes.next()) { String indexName = indexes.getString("INDEX_NAME"); @@ -107,7 +107,9 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet indexes = metadata.getColumns(null, null, null, null); + String catalog = connection.getCatalog(); + String schema = connection.getSchema(); + ResultSet indexes = metadata.getColumns(catalog, schema, theTableName, null); while (indexes.next()) { @@ -158,7 +160,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet indexes = metadata.getCrossReference(null, null, theTableName, null, null, theForeignTable); + ResultSet indexes = metadata.getCrossReference(connection.getCatalog(), connection.getSchema(), theTableName, connection.getCatalog(), connection.getSchema(), theForeignTable); Set columnNames = new HashSet<>(); while (indexes.next()) { @@ -194,7 +196,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet indexes = metadata.getColumns(null, null, null, null); + ResultSet indexes = metadata.getColumns(connection.getCatalog(), connection.getSchema(), theTableName, null); Set columnNames = new HashSet<>(); while (indexes.next()) { @@ -223,7 +225,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet tables = metadata.getTables(null, null, null, null); + ResultSet tables = metadata.getTables(connection.getCatalog(), connection.getSchema(), null, null); Set columnNames = new HashSet<>(); while (tables.next()) { @@ -254,7 +256,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet tables = metadata.getColumns(null, null, null, null); + ResultSet tables = metadata.getColumns(connection.getCatalog(), connection.getSchema(), theTableName, theColumnName); while (tables.next()) { String tableName = toUpperCase(tables.getString("TABLE_NAME"), Locale.US); diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java index 60792b73811..43753507426 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/Migrator.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.migrate; * 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. @@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class Migrator { @@ -40,6 +41,7 @@ public class Migrator { private DriverTypeEnum.ConnectionProperties myConnectionProperties; private int myChangesCount; private boolean myDryRun; + private List myExecutedStatements = new ArrayList<>(); public int getChangesCount() { return myChangesCount; @@ -74,7 +76,7 @@ public class Migrator { myConnectionProperties = myDriverType.newConnectionProperties(myConnectionUrl, myUsername, myPassword); try { - for (BaseTask next : myTasks) { + for (BaseTask next : myTasks) { next.setDriverType(myDriverType); next.setConnectionProperties(myConnectionProperties); next.setDryRun(myDryRun); @@ -85,12 +87,33 @@ public class Migrator { } myChangesCount += next.getChangesCount(); + myExecutedStatements.addAll(next.getExecutedStatements()); } } finally { myConnectionProperties.close(); } ourLog.info("Finished migration of {} tasks", myTasks.size()); + + if (myDryRun) { + StringBuilder statementBuilder = new StringBuilder(); + String lastTable = null; + for (BaseTask.ExecutedStatement next : myExecutedStatements) { + if (!Objects.equals(lastTable, next.getTableName())) { + statementBuilder.append("\n\n-- Table: ").append(next.getTableName()).append("\n"); + lastTable = next.getTableName(); + } + + statementBuilder.append(next.getSql()).append(";\n"); + + for (Object nextArg : next.getArguments()) { + statementBuilder.append(" -- Arg: ").append(nextArg).append("\n"); + } + } + + ourLog.info("SQL that would be executed:\n\n***********************************\n{}***********************************", statementBuilder); + } + } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddColumnTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddColumnTask.java index ca1a7545538..b194d1e973f 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddColumnTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddColumnTask.java @@ -46,9 +46,22 @@ public class AddColumnTask extends BaseTableColumnTypeTask { nullable = ""; } - String sql = "alter table " + getTableName() + " add column " + getColumnName() + " " + type + " " + nullable; + String sql = ""; + switch (getDriverType()) { + case DERBY_EMBEDDED: + case MARIADB_10_1: + case MYSQL_5_7: + case POSTGRES_9_4: + sql = "alter table " + getTableName() + " add column " + getColumnName() + " " + type + " " + nullable; + break; + case MSSQL_2012: + case ORACLE_12C: + sql = "alter table " + getTableName() + " add " + getColumnName() + " " + type + " " + nullable; + break; + } + ourLog.info("Adding column {} of type {} to table {}", getColumnName(), type, getTableName()); - executeSql(sql); + executeSql(getTableName(), sql); } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddForeignKeyTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddForeignKeyTask.java index a49d4808e41..4affdcd4ad0 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddForeignKeyTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddForeignKeyTask.java @@ -83,7 +83,7 @@ public class AddForeignKeyTask extends BaseTableColumnTask { try { - executeSql(sql); + executeSql(getTableName(), sql); } catch (Exception e) { if (e.toString().contains("already exists")) { ourLog.warn("Index {} already exists", myConstraintName); diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIndexTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIndexTask.java index f5e40556901..dac17bb52de 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIndexTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIndexTask.java @@ -67,12 +67,13 @@ public class AddIndexTask extends BaseTableTask { return; } - String unique = myUnique ? "UNIQUE " : ""; + String unique = myUnique ? "unique " : ""; String columns = String.join(", ", myColumns); - String sql = "CREATE " + unique + " INDEX " + myIndexName + " ON " + getTableName() + "(" + columns + ")"; + String sql = "create " + unique + "index " + myIndexName + " on " + getTableName() + "(" + columns + ")"; + String tableName = getTableName(); try { - executeSql(sql); + executeSql(tableName, sql); } catch (Exception e) { if (e.toString().contains("already exists")) { ourLog.warn("Index {} already exists", myIndexName); diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTask.java index 57a1cb481a4..9fdaefafe18 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTask.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.migrate.taskdef; * 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. @@ -38,11 +38,13 @@ public class ArbitrarySqlTask extends BaseTask { private static final Logger ourLog = LoggerFactory.getLogger(ArbitrarySqlTask.class); private final String myDescription; + private final String myTableName; private List myTask = new ArrayList<>(); private int myBatchSize = 1000; private String myExecuteOnlyIfTableExists; - public ArbitrarySqlTask(String theDescription) { + public ArbitrarySqlTask(String theTableName, String theDescription) { + myTableName = theTableName; myDescription = theDescription; } @@ -104,7 +106,6 @@ public class ArbitrarySqlTask extends BaseTask { @Override public void execute() { if (isDryRun()) { - logDryRunSql(mySql); return; } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java index b5963b63010..9ba2f97895d 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.migrate.taskdef; * 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. @@ -28,6 +28,10 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.support.TransactionTemplate; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; public abstract class BaseTask { @@ -37,6 +41,7 @@ public abstract class BaseTask { private String myDescription; private int myChangesCount; private boolean myDryRun; + private List myExecutedStatements = new ArrayList<>(); public boolean isDryRun() { return myDryRun; @@ -56,29 +61,36 @@ public abstract class BaseTask { return (T) this; } + public List getExecutedStatements() { + return myExecutedStatements; + } + public int getChangesCount() { return myChangesCount; } - public void executeSql(@Language("SQL") String theSql, Object... theArguments) { - if (isDryRun()) { - logDryRunSql(theSql); - return; + /** + * @param theTableName This is only used for logging currently + * @param theSql The SQL statement + * @param theArguments The SQL statement arguments + */ + public void executeSql(String theTableName, @Language("SQL") String theSql, Object... theArguments) { + if (isDryRun() == false) { + Integer changes = getConnectionProperties().getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = getConnectionProperties().newJdbcTemplate(); + int changesCount = jdbcTemplate.update(theSql, theArguments); + ourLog.info("SQL \"{}\" returned {}", theSql, changesCount); + return changesCount; + }); + + myChangesCount += changes; } - Integer changes = getConnectionProperties().getTxTemplate().execute(t -> { - JdbcTemplate jdbcTemplate = getConnectionProperties().newJdbcTemplate(); - int changesCount = jdbcTemplate.update(theSql, theArguments); - ourLog.info("SQL \"{}\" returned {}", theSql, changesCount); - return changesCount; - }); - - myChangesCount += changes; - + captureExecutedStatement(theTableName, theSql, theArguments); } - protected void logDryRunSql(@Language("SQL") String theSql) { - ourLog.info("WOULD EXECUTE SQL: {}", theSql); + protected void captureExecutedStatement(String theTableName, @Language("SQL") String theSql, Object[] theArguments) { + myExecutedStatements.add(new ExecutedStatement(theTableName, theSql, theArguments)); } public DriverTypeEnum.ConnectionProperties getConnectionProperties() { @@ -108,4 +120,28 @@ public abstract class BaseTask { } public abstract void execute() throws SQLException; + + public static class ExecutedStatement { + private final String mySql; + private final List myArguments; + private final String myTableName; + + public ExecutedStatement(String theDescription, String theSql, Object[] theArguments) { + myTableName = theDescription; + mySql = theSql; + myArguments = theArguments != null ? Arrays.asList(theArguments) : Collections.emptyList(); + } + + public String getTableName() { + return myTableName; + } + + public String getSql() { + return mySql; + } + + public List getArguments() { + return myArguments; + } + } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTask.java index 0300e92fa32..bf7c56ca591 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTask.java @@ -162,7 +162,7 @@ public class CalculateHashesTask extends BaseTableColumnTask arguments = new ArrayList<>(); + List arguments = new ArrayList<>(); sqlBuilder.append("UPDATE "); sqlBuilder.append(getTableName()); sqlBuilder.append(" SET "); @@ -174,7 +174,7 @@ public class CalculateHashesTask extends BaseTableColumnTask { String sql = "alter table " + getTableName() + " drop column " + getColumnName(); ourLog.info("Dropping column {} on table {}", getColumnName(), getTableName()); - executeSql(sql); + executeSql(getTableName(), sql); } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/DropIndexTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/DropIndexTask.java index 0cfbb0e3290..5c3c56f4602 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/DropIndexTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/DropIndexTask.java @@ -63,15 +63,15 @@ public class DropIndexTask extends BaseTableTask { switch (getDriverType()) { case MYSQL_5_7: case MARIADB_10_1: - sql = "ALTER TABLE " + getTableName() + " DROP INDEX " + myIndexName; + sql = "alter table " + getTableName() + " drop index " + myIndexName; break; case DERBY_EMBEDDED: - sql = "DROP INDEX " + myIndexName; + sql = "drop index " + myIndexName; break; case POSTGRES_9_4: case ORACLE_12C: case MSSQL_2012: - sql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT " + myIndexName; + sql = "alter table " + getTableName() + " drop constraint " + myIndexName; break; } } else { @@ -79,19 +79,19 @@ public class DropIndexTask extends BaseTableTask { switch (getDriverType()) { case MYSQL_5_7: case MARIADB_10_1: - sql = "ALTER TABLE " + getTableName() + " DROP INDEX " + myIndexName; + sql = "alter table " + getTableName() + " drop index " + myIndexName; break; case POSTGRES_9_4: case DERBY_EMBEDDED: case ORACLE_12C: - sql = "DROP INDEX " + myIndexName; + sql = "drop index " + myIndexName; break; case MSSQL_2012: - sql = "DROP INDEX " + getTableName() + "." + myIndexName; + sql = "drop index " + getTableName() + "." + myIndexName; break; } } - executeSql(sql); + executeSql(getTableName(), sql); } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ModifyColumnTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ModifyColumnTask.java index a748e844fde..0f772b171d6 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ModifyColumnTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ModifyColumnTask.java @@ -95,12 +95,12 @@ public class ModifyColumnTask extends BaseTableColumnTypeTask ourLog.info("Updating column {} on table {} to type {}", getColumnName(), getTableName(), type); if (sql != null) { - executeSql(sql); + executeSql(getTableName(), sql); } if (sqlNotNull != null) { ourLog.info("Updating column {} on table {} to not null", getColumnName(), getTableName()); - executeSql(sqlNotNull); + executeSql(getTableName(), sqlNotNull); } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 68c0d804e58..821d11832b4 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -322,7 +322,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .unique(false) .withColumns("HASH_PRESENCE"); - ArbitrarySqlTask consolidateSearchParamPresenceIndexesTask = new ArbitrarySqlTask("Consolidate search parameter presence indexes"); + ArbitrarySqlTask consolidateSearchParamPresenceIndexesTask = new ArbitrarySqlTask("HFJ_SEARCH_PARM", "Consolidate search parameter presence indexes"); consolidateSearchParamPresenceIndexesTask.setExecuteOnlyIfTableExists("HFJ_SEARCH_PARM"); consolidateSearchParamPresenceIndexesTask.setBatchSize(1); @@ -338,7 +338,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { String resType = (String) t.get("RES_TYPE"); String paramName = (String) t.get("PARAM_NAME"); Long hash = SearchParamPresent.calculateHashPresence(resType, paramName, present); - consolidateSearchParamPresenceIndexesTask.executeSql("update HFJ_RES_PARAM_PRESENT set HASH_PRESENCE = ? where PID = ?", hash, pid); + consolidateSearchParamPresenceIndexesTask.executeSql("HFJ_RES_PARAM_PRESENT", "update HFJ_RES_PARAM_PRESENT set HASH_PRESENCE = ? where PID = ?", hash, pid); }); version.addTask(consolidateSearchParamPresenceIndexesTask); diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java index 0dc2670cc01..df904798093 100644 --- a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java @@ -19,7 +19,7 @@ public class ArbitrarySqlTaskTest extends BaseTest { executeSql("insert into HFJ_RES_PARAM_PRESENT (PID, SP_ID, SP_PRESENT, HASH_PRESENT) values (100, 1, true, null)"); executeSql("insert into HFJ_RES_PARAM_PRESENT (PID, SP_ID, SP_PRESENT, HASH_PRESENT) values (101, 2, true, null)"); - ArbitrarySqlTask task = new ArbitrarySqlTask("Consolidate search parameter presence indexes"); + ArbitrarySqlTask task = new ArbitrarySqlTask("HFJ_RES_PARAM_PRESENT", "Consolidate search parameter presence indexes"); task.setExecuteOnlyIfTableExists("hfj_search_parm"); task.setBatchSize(1); String sql = "SELECT " + @@ -34,7 +34,7 @@ public class ArbitrarySqlTaskTest extends BaseTest { String resType = (String) t.get("RES_TYPE"); String paramName = (String) t.get("PARAM_NAME"); Long hash = SearchParamPresent.calculateHashPresence(resType, paramName, present); - task.executeSql("update HFJ_RES_PARAM_PRESENT set HASH_PRESENT = ? where PID = ?", hash, pid); + task.executeSql("HFJ_RES_PARAM_PRESENT", "update HFJ_RES_PARAM_PRESENT set HASH_PRESENT = ? where PID = ?", hash, pid); }); getMigrator().addTask(task); @@ -53,11 +53,11 @@ public class ArbitrarySqlTaskTest extends BaseTest { @Test public void testExecuteOnlyIfTableExists() { - ArbitrarySqlTask task = new ArbitrarySqlTask("Consolidate search parameter presence indexes"); + ArbitrarySqlTask task = new ArbitrarySqlTask("HFJ_RES_PARAM_PRESENT", "Consolidate search parameter presence indexes"); task.setBatchSize(1); String sql = "SELECT * FROM HFJ_SEARCH_PARM"; task.addQuery(sql, ArbitrarySqlTask.QueryModeEnum.BATCH_UNTIL_NO_MORE, t -> { - task.executeSql("update HFJ_RES_PARAM_PRESENT set FOOFOOOFOO = null"); + task.executeSql("HFJ_RES_PARAM_PRESENT", "update HFJ_RES_PARAM_PRESENT set FOOFOOOFOO = null"); }); task.setExecuteOnlyIfTableExists("hfj_search_parm"); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index ea7d951f3ce..7203d2775cb 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml @@ -158,7 +158,7 @@ ca.uhn.hapi.fhir hapi-fhir-converter - 3.6.0 + 3.7.0-SNAPSHOT diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index a70c9399b8b..f5d2d3d67ce 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhirtest.config.*; import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor; import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -147,6 +148,7 @@ public class TestRestfulServer extends RestfulServer { confProvider.setImplementationDescription(implDesc); setServerConformanceProvider(confProvider); plainProviders.add(myAppCtx.getBean(TerminologyUploaderProviderR4.class)); + plainProviders.add(myAppCtx.getBean(GraphQLProvider.class)); break; } default: diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java index 5620f1f67ef..c306cf66dae 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java @@ -154,4 +154,7 @@ public class TestR4Config extends BaseJavaConfigR4 { public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } + + + } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/interceptor/PublicSecurityInterceptor.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/interceptor/PublicSecurityInterceptor.java index ac31103ba0f..200a0f9cff1 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/interceptor/PublicSecurityInterceptor.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/interceptor/PublicSecurityInterceptor.java @@ -34,11 +34,11 @@ public class PublicSecurityInterceptor extends AuthorizationInterceptor { if (isBlank(authHeader)) { return new RuleBuilder() - .deny().operation().named(BaseJpaSystemProvider.MARK_ALL_RESOURCES_FOR_REINDEXING).onServer().andThen() - .deny().operation().named(BaseTerminologyUploaderProvider.UPLOAD_EXTERNAL_CODE_SYSTEM).onServer().andThen() - .deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onServer().andThen() - .deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onAnyType().andThen() - .deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onAnyInstance().andThen() + .deny().operation().named(BaseJpaSystemProvider.MARK_ALL_RESOURCES_FOR_REINDEXING).onServer().andAllowAllResponses().andThen() + .deny().operation().named(BaseTerminologyUploaderProvider.UPLOAD_EXTERNAL_CODE_SYSTEM).onServer().andAllowAllResponses().andThen() + .deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onServer().andAllowAllResponses().andThen() + .deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onAnyType().andAllowAllResponses().andThen() + .deny().operation().named(JpaConstants.OPERATION_EXPUNGE).onAnyInstance().andAllowAllResponses().andThen() .allowAll() .build(); } diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index ce10f77c9ea..3ea4bbff59f 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java index 6c3e2be0607..28effed69c1 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java @@ -20,7 +20,6 @@ import java.io.Reader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.*; -import java.util.function.BiFunction; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -55,7 +54,7 @@ public abstract class RequestDetails { private String myOperation; private Map myParameters; private byte[] myRequestContents; - private IRequestOperationCallback myRequestOperationCallback = new RequestOperationCallback(); + private IRequestOperationCallback myRequestOperationCallback; private String myRequestPath; private RequestTypeEnum myRequestType; private String myResourceName; @@ -67,6 +66,13 @@ public abstract class RequestDetails { private Map> myUnqualifiedToQualifiedNames; private Map myUserData; + /** + * Constructor + */ + public RequestDetails() { + myRequestOperationCallback = new RequestOperationCallback(); + } + public void addParameter(String theName, String[] theValues) { getParameters(); myParameters.put(theName, theValues); @@ -406,6 +412,94 @@ public abstract class RequestDetails { myRequestContents = theRequestContents; } + /** + * Sets the {@link #getRequestOperationCallback() requestOperationCallback} handler in + * deferred mode, meaning that any notifications will be queued up for delivery, but + * won't be delivered until {@link #stopDeferredRequestOperationCallbackAndRunDeferredItems()} + * is called. + */ + public void startDeferredOperationCallback() { + myRequestOperationCallback = new DeferredOperationCallback(myRequestOperationCallback); + } + + /** + * @see #startDeferredOperationCallback() + */ + public void stopDeferredRequestOperationCallbackAndRunDeferredItems() { + DeferredOperationCallback deferredCallback = (DeferredOperationCallback) myRequestOperationCallback; + deferredCallback.playDeferredActions(); + myRequestOperationCallback = deferredCallback.getWrap(); + } + + + private class DeferredOperationCallback implements IRequestOperationCallback { + + private final IRequestOperationCallback myWrap; + private final List myDeferredTasks = new ArrayList<>(); + + private DeferredOperationCallback(IRequestOperationCallback theWrap) { + myWrap = theWrap; + } + + @Override + public void resourceCreated(IBaseResource theResource) { + myDeferredTasks.add(()-> myWrap.resourceCreated(theResource)); + } + + @Override + public void resourceDeleted(IBaseResource theResource) { + myDeferredTasks.add(()-> myWrap.resourceDeleted(theResource)); + } + + @Override + public void resourcePreCreate(IBaseResource theResource) { + myWrap.resourcePreCreate(theResource); + } + + @Override + public void resourcePreDelete(IBaseResource theResource) { + myWrap.resourcePreDelete(theResource); + } + + @Override + public void resourcePreUpdate(IBaseResource theOldResource, IBaseResource theNewResource) { + myWrap.resourcePreUpdate(theOldResource, theNewResource); + } + + @Override + public void resourceUpdated(IBaseResource theResource) { + myDeferredTasks.add(()-> myWrap.resourceUpdated(theResource)); + } + + @Override + public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { + myDeferredTasks.add(()-> myWrap.resourceUpdated(theOldResource, theNewResource)); + } + + @Override + public void resourcesCreated(Collection theResource) { + myDeferredTasks.add(()-> myWrap.resourcesCreated(theResource)); + } + + @Override + public void resourcesDeleted(Collection theResource) { + myDeferredTasks.add(()-> myWrap.resourcesDeleted(theResource)); + } + + @Override + public void resourcesUpdated(Collection theResource) { + myDeferredTasks.add(()-> myWrap.resourcesUpdated(theResource)); + } + + void playDeferredActions() { + myDeferredTasks.forEach(Runnable::run); + } + + IRequestOperationCallback getWrap() { + return myWrap; + } + } + private class RequestOperationCallback implements IRequestOperationCallback { private List getInterceptors() { @@ -499,6 +593,7 @@ public abstract class RequestDetails { /** * @deprecated Deprecated in HAPI FHIR 2.6 - Use {@link IRequestOperationCallback#resourceUpdated(IBaseResource, IBaseResource)} instead */ + @Override @Deprecated public void resourcesUpdated(Collection theResource) { for (IBaseResource next : theResource) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index d24e27250ff..4a73f65dea6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server; * 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. @@ -346,20 +346,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer theList) { + myInterceptors.clear(); + if (theList != null) { + myInterceptors.addAll(theList); + } + } + /** * Sets (or clears) the list of interceptors * @@ -578,18 +576,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer theList) { - myInterceptors.clear(); - if (theList != null) { - myInterceptors.addAll(theList); - } - } - @Override public IPagingProvider getPagingProvider() { return myPagingProvider; @@ -616,13 +602,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer theProviders) { - Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); - - myPlainProviders.clear(); - if (theProviders != null) { - myPlainProviders.addAll(theProviders); - } + public void setPlainProviders(Object... theProv) { + setPlainProviders(Arrays.asList(theProv)); } /** @@ -630,8 +611,13 @@ public class RestfulServer extends HttpServlet implements IRestfulServer theProviders) { + Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); + + myPlainProviders.clear(); + if (theProviders != null) { + myPlainProviders.addAll(theProviders); + } } /** @@ -643,7 +629,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer theProviders) { - Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); - + public void setResourceProviders(IResourceProvider... theResourceProviders) { myResourceProviders.clear(); - if (theProviders != null) { - myResourceProviders.addAll(theProviders); + if (theResourceProviders != null) { + myResourceProviders.addAll(Arrays.asList(theResourceProviders)); } } /** * Sets the resource providers for this server */ - public void setResourceProviders(IResourceProvider... theResourceProviders) { + public void setResourceProviders(Collection theProviders) { + Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); + myResourceProviders.clear(); - if (theResourceProviders != null) { - myResourceProviders.addAll(Arrays.asList(theResourceProviders)); + if (theProviders != null) { + myResourceProviders.addAll(theProviders); } } @@ -1648,6 +1635,20 @@ public class RestfulServer extends HttpServlet implements IRestfulServerserver level */ - IAuthRuleBuilderRuleOpClassifierFinished onServer(); + IAuthRuleBuilderOperationNamedAndScoped onServer(); /** * Rule applies to invocations of this operation at the type level */ - IAuthRuleBuilderRuleOpClassifierFinished onType(Class theType); + IAuthRuleBuilderOperationNamedAndScoped onType(Class theType); /** * Rule applies to invocations of this operation at the type level on any type */ - IAuthRuleBuilderRuleOpClassifierFinished onAnyType(); + IAuthRuleBuilderOperationNamedAndScoped onAnyType(); /** * Rule applies to invocations of this operation at the instance level */ - IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId); + IAuthRuleBuilderOperationNamedAndScoped onInstance(IIdType theInstanceId); /** * Rule applies to invocations of this operation at the instance level on any instance of the given type */ - IAuthRuleBuilderRuleOpClassifierFinished onInstancesOfType(Class theType); + IAuthRuleBuilderOperationNamedAndScoped onInstancesOfType(Class theType); /** * Rule applies to invocations of this operation at the instance level on any instance */ - IAuthRuleBuilderRuleOpClassifierFinished onAnyInstance(); + IAuthRuleBuilderOperationNamedAndScoped onAnyInstance(); /** * Rule applies to invocations of this operation at any level (server, type or instance) */ - IAuthRuleBuilderRuleOpClassifierFinished atAnyLevel(); + IAuthRuleBuilderOperationNamedAndScoped atAnyLevel(); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperationNamedAndScoped.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperationNamedAndScoped.java new file mode 100644 index 00000000000..1bc5a851766 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperationNamedAndScoped.java @@ -0,0 +1,19 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +public interface IAuthRuleBuilderOperationNamedAndScoped { + + /** + * Responses for this operation will not be checked + */ + IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponses(); + + /** + * Responses for this operation must be authorized by other rules. For example, if this + * rule is authorizing the Patient $everything operation, there must be a separate + * rule (or rules) that actually authorize the user to read the + * resources being returned + */ + IAuthRuleBuilderRuleOpClassifierFinished andRequireExplicitResponseAuthorization(); + + +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java index 4f68b723377..b629bdabaf5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java @@ -108,4 +108,8 @@ public interface IAuthRuleBuilderRule { */ IAuthRuleBuilderRuleOp write(); + /** + * Allow a GraphQL query + */ + IAuthRuleBuilderGraphQL graphQL(); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java index fcb0f1e8911..88c129ce2bc 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * 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. @@ -41,6 +41,7 @@ class OperationRule extends BaseRule implements IAuthRule { private boolean myAppliesToAnyType; private boolean myAppliesToAnyInstance; private boolean myAppliesAtAnyLevel; + private boolean myAllowAllResponses; OperationRule(String theRuleName) { super(theRuleName); @@ -50,6 +51,10 @@ class OperationRule extends BaseRule implements IAuthRule { myAppliesAtAnyLevel = theAppliesAtAnyLevel; } + public void allowAllResponses() { + myAllowAllResponses = true; + } + void appliesToAnyInstance() { myAppliesToAnyInstance = true; } @@ -114,23 +119,32 @@ class OperationRule extends BaseRule implements IAuthRule { case EXTENDED_OPERATION_INSTANCE: if (myAppliesToAnyInstance || myAppliesAtAnyLevel) { applies = true; - } else if (theInputResourceId != null) { - if (myAppliesToIds != null) { - String instanceId = theInputResourceId.toUnqualifiedVersionless().getValue(); - for (IIdType next : myAppliesToIds) { - if (next.toUnqualifiedVersionless().getValue().equals(instanceId)) { - applies = true; - break; + } else { + IIdType requestResourceId = null; + if (theInputResourceId != null) { + requestResourceId = theInputResourceId; + } + if (requestResourceId == null && myAllowAllResponses) { + requestResourceId = theRequestDetails.getId(); + } + if (requestResourceId != null) { + if (myAppliesToIds != null) { + String instanceId = requestResourceId .toUnqualifiedVersionless().getValue(); + for (IIdType next : myAppliesToIds) { + if (next.toUnqualifiedVersionless().getValue().equals(instanceId)) { + applies = true; + break; + } } } - } - if (myAppliesToInstancesOfType != null) { - // TODO: Convert to a map of strings and keep the result - for (Class next : myAppliesToInstancesOfType) { - String resName = ctx.getResourceDefinition(next).getName(); - if (resName.equals(theInputResourceId.getResourceType())) { - applies = true; - break; + if (myAppliesToInstancesOfType != null) { + // TODO: Convert to a map of strings and keep the result + for (Class next : myAppliesToInstancesOfType) { + String resName = ctx.getResourceDefinition(next).getName(); + if (resName.equals(requestResourceId .getResourceType())) { + applies = true; + break; + } } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index ef003145009..dd93313975e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -237,6 +237,11 @@ public class RuleBuilder implements IAuthRuleBuilder { return new RuleBuilderRuleOp(); } + @Override + public IAuthRuleBuilderGraphQL graphQL() { + return new RuleBuilderGraphQL(); + } + private class RuleBuilderRuleConditional implements IAuthRuleBuilderRuleConditional { private AppliesTypeEnum myAppliesTo; @@ -411,6 +416,28 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderRuleOperationNamed implements IAuthRuleBuilderOperationNamed { + private class RuleBuilderOperationNamedAndScoped implements IAuthRuleBuilderOperationNamedAndScoped { + + private final OperationRule myRule; + + public RuleBuilderOperationNamedAndScoped(OperationRule theRule) { + myRule = theRule; + } + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponses() { + myRule.allowAllResponses(); + myRules.add(myRule); + return new RuleBuilderFinished(myRule); + } + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished andRequireExplicitResponseAuthorization() { + myRules.add(myRule); + return new RuleBuilderFinished(myRule); + } + } + private String myOperationName; RuleBuilderRuleOperationNamed(String theOperationName) { @@ -429,31 +456,28 @@ public class RuleBuilder implements IAuthRuleBuilder { } @Override - public IAuthRuleBuilderRuleOpClassifierFinished onAnyInstance() { + public IAuthRuleBuilderOperationNamedAndScoped onAnyInstance() { OperationRule rule = createRule(); rule.appliesToAnyInstance(); - myRules.add(rule); - return new RuleBuilderFinished(rule); + return new RuleBuilderOperationNamedAndScoped(rule); } @Override - public IAuthRuleBuilderRuleOpClassifierFinished atAnyLevel() { + public IAuthRuleBuilderOperationNamedAndScoped atAnyLevel() { OperationRule rule = createRule(); rule.appliesAtAnyLevel(true); - myRules.add(rule); - return new RuleBuilderFinished(rule); + return new RuleBuilderOperationNamedAndScoped(rule); } @Override - public IAuthRuleBuilderRuleOpClassifierFinished onAnyType() { + public IAuthRuleBuilderOperationNamedAndScoped onAnyType() { OperationRule rule = createRule(); rule.appliesToAnyType(); - myRules.add(rule); - return new RuleBuilderFinished(rule); + return new RuleBuilderOperationNamedAndScoped(rule); } @Override - public IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId) { + public IAuthRuleBuilderOperationNamedAndScoped onInstance(IIdType theInstanceId) { Validate.notNull(theInstanceId, "theInstanceId must not be null"); Validate.notBlank(theInstanceId.getResourceType(), "theInstanceId does not have a resource type"); Validate.notBlank(theInstanceId.getIdPart(), "theInstanceId does not have an ID part"); @@ -462,36 +486,32 @@ public class RuleBuilder implements IAuthRuleBuilder { ArrayList ids = new ArrayList<>(); ids.add(theInstanceId); rule.appliesToInstances(ids); - myRules.add(rule); - return new RuleBuilderFinished(rule); + return new RuleBuilderOperationNamedAndScoped(rule); } @Override - public IAuthRuleBuilderRuleOpClassifierFinished onInstancesOfType(Class theType) { + public IAuthRuleBuilderOperationNamedAndScoped onInstancesOfType(Class theType) { validateType(theType); OperationRule rule = createRule(); rule.appliesToInstancesOfType(toTypeSet(theType)); - myRules.add(rule); - return new RuleBuilderFinished(rule); + return new RuleBuilderOperationNamedAndScoped(rule); } @Override - public IAuthRuleBuilderRuleOpClassifierFinished onServer() { + public IAuthRuleBuilderOperationNamedAndScoped onServer() { OperationRule rule = createRule(); rule.appliesToServer(); - myRules.add(rule); - return new RuleBuilderFinished(rule); + return new RuleBuilderOperationNamedAndScoped(rule); } @Override - public IAuthRuleBuilderRuleOpClassifierFinished onType(Class theType) { + public IAuthRuleBuilderOperationNamedAndScoped onType(Class theType) { validateType(theType); OperationRule rule = createRule(); rule.appliesToTypes(toTypeSet(theType)); - myRules.add(rule); - return new RuleBuilderFinished(rule); + return new RuleBuilderOperationNamedAndScoped(rule); } private HashSet> toTypeSet(Class theType) { @@ -543,6 +563,17 @@ public class RuleBuilder implements IAuthRuleBuilder { return new RuleBuilderFinished(rule); } } + + private class RuleBuilderGraphQL implements IAuthRuleBuilderGraphQL { + @Override + public IAuthRuleFinished any() { + RuleImplOp rule = new RuleImplOp(myRuleName); + rule.setOp(RuleOpEnum.GRAPHQL); + rule.setMode(myRuleMode); + myRules.add(rule); + return new RuleBuilderFinished(rule); + } + } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index b0e0c320b75..58b4b8cb47d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -174,6 +174,12 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { return null; } break; + case GRAPHQL: + if (theOperation == RestOperationTypeEnum.GRAPHQL_REQUEST) { + return newVerdict(); + } else { + return null; + } case TRANSACTION: if (!(theOperation == RestOperationTypeEnum.TRANSACTION)) { return null; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java index 4104086bb65..6f58a9ad29c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java @@ -32,5 +32,6 @@ enum RuleOpEnum { METADATA, DELETE, OPERATION, + GRAPHQL, PATCH } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java index 969c0fa12b6..da498b2cd10 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java @@ -50,6 +50,7 @@ public class ServletRestfulResponse extends RestfulResponse ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/test/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfigurationTest.java b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/test/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfigurationTest.java index 7f2600300ce..72c7df521d0 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/test/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfigurationTest.java +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/test/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfigurationTest.java @@ -14,6 +14,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.spring.boot.autoconfigure.FhirAutoConfiguration.FhirJpaServerConfiguration.Dstu3; import org.assertj.core.util.Arrays; import org.junit.After; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -55,10 +56,19 @@ public class FhirAutoConfigurationTest { @Test public void withFhirVersion() throws Exception { - load("hapi.fhir.version:DSTU3"); + load(Arrays.array(EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class, + FhirAutoConfiguration.class), + "hapi.fhir.version:DSTU3", "spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles", + "spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory"); assertThat(this.context.getBean(FhirContext.class).getVersion()).isEqualTo(FhirVersionEnum.DSTU3.getVersionImplementation()); - load("hapi.fhir.version:R4"); + load(Arrays.array(EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class, + FhirAutoConfiguration.class), + "hapi.fhir.version:R4", + "spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles", + "spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory"); assertThat(this.context.getBean(FhirContext.class).getVersion()).isEqualTo(FhirVersionEnum.R4.getVersionImplementation()); } diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml index 8b5bf8dbd18..1f848c7f84c 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 3.6.0 + 3.7.0-SNAPSHOT hapi-fhir-spring-boot-sample-client-apache diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml index 60002ac52d2..b2a7d1389a4 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 3.6.0 + 3.7.0-SNAPSHOT hapi-fhir-spring-boot-sample-client-okhttp diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index db094161df7..ddedb91522d 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 3.6.0 + 3.7.0-SNAPSHOT hapi-fhir-spring-boot-sample-server-jersey diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/pom.xml index 3101fc961e5..43b9dae672c 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 3.6.0 + 3.7.0-SNAPSHOT hapi-fhir-spring-boot-sample-server-jpa diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/src/test/java/sample/fhir/server/jpa/SampleJpaRestfulServerApplicationTest.java b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/src/test/java/sample/fhir/server/jpa/SampleJpaRestfulServerApplicationTest.java index bc36ba6eb25..a94bb83fea0 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/src/test/java/sample/fhir/server/jpa/SampleJpaRestfulServerApplicationTest.java +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/src/test/java/sample/fhir/server/jpa/SampleJpaRestfulServerApplicationTest.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.client.api.IGenericClient; import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index 52f341659de..153f1fa3d7a 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot - 3.6.0 + 3.7.0-SNAPSHOT hapi-fhir-spring-boot-samples diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index 387b6388d9f..86ecbc36d5f 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index f01a897c7dc..1e3fc36f83a 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index b81f58818d7..5e0d5a2c16f 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 0d239c3d1b7..26ff214acf0 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java index fe533d76b7c..8d59634cd28 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java @@ -572,7 +572,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().withAnyName().onServer().andThen() + .allow("RULE 1").operation().withAnyName().onServer().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -598,7 +598,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization() .build(); } }); @@ -633,7 +633,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andThen() + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().andThen() .allow("Rule 2").read().allResources().inCompartment("Patient", new IdDt("Patient/1")).andThen() .build(); } @@ -671,7 +671,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization() .build(); } }); @@ -705,7 +705,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onInstance(new IdDt("http://example.com/Patient/1/_history/2")).andThen() + .allow("RULE 1").operation().named("opName").onInstance(new IdDt("http://example.com/Patient/1/_history/2")).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -764,7 +764,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onAnyInstance().andThen() + .allow("RULE 1").operation().named("opName").onAnyInstance().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -890,7 +890,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onServer().andThen() + .allow("RULE 1").operation().named("opName").onServer().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -937,7 +937,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onType(Patient.class).andThen() + .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1006,7 +1006,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onAnyType().andThen() + .allow("RULE 1").operation().named("opName").onAnyType().andRequireExplicitResponseAuthorization().andThen() .build(); } }); diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 9f54169d028..9d6604bca4c 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/CustomDstu3ClassWithDstu2Base.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/CustomDstu3ClassWithDstu2Base.java new file mode 100644 index 00000000000..1dc2ecfe6cb --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/CustomDstu3ClassWithDstu2Base.java @@ -0,0 +1,238 @@ +package ca.uhn.fhir.context; + +import ca.uhn.fhir.model.api.BaseIdentifiableElement; +import ca.uhn.fhir.model.api.IElement; +import ca.uhn.fhir.model.api.IExtension; +import ca.uhn.fhir.model.api.annotation.*; +import ca.uhn.fhir.model.api.annotation.Extension; +import org.hl7.fhir.dstu3.model.*; + +import java.util.List; + +@ResourceDef(name = "ResourceWithExtensionsA", id="0001") +public class CustomDstu3ClassWithDstu2Base extends DomainResource { + + /* + * NB: several unit tests depend on the structure here + * so check the unit tests immediately after any changes + */ + + private static final long serialVersionUID = 1L; + + @Child(name = "foo1", type = StringType.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) + @Extension(url = "http://foo/#f1", definedLocally=true, isModifier=false) + private List myFoo1; + + @Child(name = "foo2", type = StringType.class, order = 1, min = 0, max = 1) + @Extension(url = "http://foo/#f2", definedLocally=true, isModifier=true) + private StringType myFoo2; + + @Child(name = "bar1", type = Bar1.class, order = 2, min = 1, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b1", definedLocally=true, isModifier=false) + private List myBar1; + + @Child(name = "bar2", type = Bar1.class, order = 3, min = 1, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b2", definedLocally=true, isModifier=false) + private Bar1 myBar2; + + @Child(name="baz", type = CodeableConcept.class, order = 4) + @Extension(url= "http://baz/#baz", definedLocally=true, isModifier=false) + @Description(shortDefinition = "Contains a codeable concept") + private CodeableConcept myBaz; + + @Child(name = "identifier", type = Identifier.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) + private List myIdentifier; + + public List getBar1() { + return myBar1; + } + + public Bar1 getBar2() { + return myBar2; + } + + public List getFoo1() { + return myFoo1; + } + + public StringType getFoo2() { + return myFoo2; + } + + public CodeableConcept getBaz() { return myBaz; } + + public List getIdentifier() { + return myIdentifier; + } + + public void setBar1(List theBar1) { + myBar1 = theBar1; + } + + public void setBar2(Bar1 theBar2) { + myBar2 = theBar2; + } + + public void setFoo1(List theFoo1) { + myFoo1 = theFoo1; + } + + public void setFoo2(StringType theFoo2) { + myFoo2 = theFoo2; + } + + public void setBaz(CodeableConcept myBaz) { this.myBaz = myBaz; } + + public void setIdentifier(List theValue) { + myIdentifier = theValue; + } + + @Block(name = "Bar1") + public static class Bar1 extends BaseIdentifiableElement implements IExtension { + + public Bar1() { + super(); + } + + @Child(name = "bar11", type = DateType.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b1/1", definedLocally=true, isModifier=false) + private List myBar11; + + @Child(name = "bar12", type = DateType.class, order = 1, min = 0, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b1/2", definedLocally=true, isModifier=false) + private List myBar12; + + private IdType myId; + + @Override + public boolean isEmpty() { + return false; // not implemented + } + + @Override + public List getAllPopulatedChildElementsOfType(Class theType) { + return ca.uhn.fhir.util.ElementUtil.allPopulatedChildElements(theType ); // not implemented + } + + + public List getBar11() { + return myBar11; + } + + public List getBar12() { + return myBar12; + } + + public void setBar11(List theBar11) { + myBar11 = theBar11; + } + + public void setBar12(List theBar12) { + myBar12 = theBar12; + } + + + + } + + @Block(name = "Bar2") + public static class Bar2 extends BaseIdentifiableElement implements IExtension { + + @Child(name = "bar121", type = DateType.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b1/2/1", definedLocally=true, isModifier=false) + private List myBar121; + + @Child(name = "bar122", type = DateType.class, order = 1, min = 0, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b1/2/2", definedLocally=true, isModifier=false) + private List myBar122; + + @Override + public boolean isEmpty() { + return false; // not implemented + } + + @Override + public List getAllPopulatedChildElementsOfType(Class theType) { + return ca.uhn.fhir.util.ElementUtil.allPopulatedChildElements(theType ); // not implemented + } + + + public List getBar121() { + return myBar121; + } + + public List getBar122() { + return myBar122; + } + + public void setBar121(List theBar121) { + myBar121 = theBar121; + } + + public void setBar122(List theBar122) { + myBar122 = theBar122; + } + + + + } + + @Override + public boolean isEmpty() { + return false; // not implemented + } + + + + @Override + public String getId() { + return null; + } + + @Override + public IdType getIdElement() { + return null; + } + + @Override + public CodeType getLanguageElement() { + return null; + } + + @Override + public Resource setId(String theId) { + return null; + } + + @Override + public Meta getMeta() { + return null; + } + + @Override + public Resource setIdElement(IdType theIdType) { + return null; + } + + @Override + public String fhirType() { + return null; + } + + @Override + protected void listChildren(List theResult) { + // nothing + } + + @Override + public DomainResource copy() { + return null; + } + + @Override + public ResourceType getResourceType() { + return null; + } + + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/FhirContextDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/FhirContextDstu3Test.java index 80a04f1f097..1ece9388c92 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/FhirContextDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/FhirContextDstu3Test.java @@ -3,16 +3,12 @@ package ca.uhn.fhir.context; import ca.uhn.fhir.rest.client.MyPatientWithExtensions; import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender; -import org.hl7.fhir.dstu3.model.Patient; -import org.hl7.fhir.dstu3.model.Reference; -import org.hl7.fhir.dstu3.model.StructureDefinition; import org.junit.AfterClass; import org.junit.Test; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -148,6 +144,34 @@ public class FhirContextDstu3Test { assertEquals(null, genderChild.getBoundEnumType()); } + /** + * See #944 + */ + @Test + public void testNullPointerException() { + Bundle bundle = new Bundle(); + MyEpisodeOfCareFHIR myEpisodeOfCare = new MyEpisodeOfCareFHIR(); + _MyReferralInformationComponent myReferralInformation = new _MyReferralInformationComponent(); + myReferralInformation._setReferralType(new Coding("someSystem", "someCode", "someDisplay")); + myReferralInformation._setFreeChoice(new Coding("someSystem2", "someCode", "someDisplay2")); + myReferralInformation._setReceived(new DateTimeType(createDate(2017, Calendar.JULY, 31))); + myReferralInformation._setReferringOrganisation(new Reference().setReference("someReference").setDisplay("someDisplay3")); + myEpisodeOfCare._setReferralInformation(myReferralInformation); + bundle.addEntry().setResource(myEpisodeOfCare); + FhirContext ctx = FhirContext.forDstu3(); + ctx.newXmlParser().encodeResourceToString(bundle); + } + + private static Date createDate( + int year, + int month, + int day) { + Calendar CAL = Calendar.getInstance(); + CAL.clear(); + CAL.set(year, month, day); + return CAL.getTime(); + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/ModelScannerDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/ModelScannerDstu3Test.java index edb42d95db3..198ef7b9b8d 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/ModelScannerDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/ModelScannerDstu3Test.java @@ -1,29 +1,21 @@ package ca.uhn.fhir.context; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; - -import java.util.List; - -import org.hl7.fhir.dstu3.model.*; -import org.junit.AfterClass; -import org.junit.Ignore; -import org.junit.Test; - import ca.uhn.fhir.model.api.annotation.*; import ca.uhn.fhir.model.api.annotation.Extension; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.dstu3.model.*; +import org.junit.AfterClass; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; public class ModelScannerDstu3Test { - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - @Test public void testScanBundle() { FhirContext ctx = FhirContext.forDstu3(); @@ -44,7 +36,9 @@ public class ModelScannerDstu3Test { } } - /** This failed at one point */ + /** + * This failed at one point + */ @Test public void testCarePlan() throws DataFormatException { FhirContext.forDstu3().getResourceDefinition(CarePlan.class); @@ -106,6 +100,17 @@ public class ModelScannerDstu3Test { } + @Test + public void testScanDstu3TypeWithDstu2Backend() throws DataFormatException { + FhirContext ctx = FhirContext.forDstu3(); + try { + ctx.getResourceDefinition(CustomDstu3ClassWithDstu2Base.class); + fail(); + } catch (ConfigurationException e) { + assertEquals("@Block class for version DSTU3 should not extend BaseIdentifiableElement: ca.uhn.fhir.context.CustomDstu3ClassWithDstu2Base$Bar1", e.getMessage()); + } + } + /** * TODO: Re-enable this when Claim's compartment defs are cleaned up */ @@ -130,21 +135,40 @@ public class ModelScannerDstu3Test { } } - @ResourceDef(name = "Patient") - public static class CompartmentForNonReferenceParam extends Patient { + /** + * See #504 + */ + @Test + public void testBinaryMayNotHaveExtensions() { + FhirContext ctx = FhirContext.forDstu3(); + try { + ctx.getResourceDefinition(LetterTemplate.class); + fail(); + } catch (ConfigurationException e) { + assertEquals("Class \"class ca.uhn.fhir.context.ModelScannerDstu3Test$LetterTemplate\" is invalid. This resource type is not a DomainResource, it must not have extensions", e.getMessage()); + } + } + + class NoResourceDef extends Patient { + @SearchParamDefinition(name = "foo", path = "Patient.telecom", type = "bar") + public static final String SP_TELECOM = "foo"; private static final long serialVersionUID = 1L; - @SearchParamDefinition(name = "foo", path = "Patient.telecom", type = "string", providesMembershipIn = { @Compartment(name = "Patient"), @Compartment(name = "Device") }) + } + + @ResourceDef(name = "Patient") + public static class CompartmentForNonReferenceParam extends Patient { + @SearchParamDefinition(name = "foo", path = "Patient.telecom", type = "string", providesMembershipIn = {@Compartment(name = "Patient"), @Compartment(name = "Device")}) public static final String SP_TELECOM = "foo"; + private static final long serialVersionUID = 1L; } @ResourceDef(name = "Patient") public static class InvalidParamType extends Patient { - private static final long serialVersionUID = 1L; - @SearchParamDefinition(name = "foo", path = "Patient.telecom", type = "bar") public static final String SP_TELECOM = "foo"; + private static final long serialVersionUID = 1L; } @@ -204,33 +228,11 @@ public class ModelScannerDstu3Test { } - class NoResourceDef extends Patient { - private static final long serialVersionUID = 1L; - - @SearchParamDefinition(name = "foo", path = "Patient.telecom", type = "bar") - public static final String SP_TELECOM = "foo"; - - } - - /** - * See #504 - */ - @Test - public void testBinaryMayNotHaveExtensions() { - FhirContext ctx = FhirContext.forDstu3(); - try { - ctx.getResourceDefinition(LetterTemplate.class); - fail(); - } catch (ConfigurationException e) { - assertEquals("Class \"class ca.uhn.fhir.context.ModelScannerDstu3Test$LetterTemplate\" is invalid. This resource type is not a DomainResource, it must not have extensions", e.getMessage()); - } - } - @ResourceDef(name = "Binary", id = "letter-template", profile = "http://www.something.org/StructureDefinition/letter-template") public static class LetterTemplate extends Binary { private static final long serialVersionUID = 1L; - + @Child(name = "name") @Extension(url = "http://example.com/dontuse#name", definedLocally = false, isModifier = false) @Description(shortDefinition = "The name of the template") @@ -239,13 +241,18 @@ public class ModelScannerDstu3Test { public LetterTemplate() { } - public void setName(StringDt name) { - myName = name; - } - public StringDt getName() { return myName; } + + public void setName(StringDt name) { + myName = name; + } + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); } } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/MyEpisodeOfCareFHIR.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/MyEpisodeOfCareFHIR.java new file mode 100644 index 00000000000..4925ba21881 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/MyEpisodeOfCareFHIR.java @@ -0,0 +1,867 @@ +package ca.uhn.fhir.context; + +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.model.api.annotation.ResourceDef; +import ca.uhn.fhir.util.ElementUtil; +import org.hl7.fhir.dstu3.model.*; + +import java.util.ArrayList; +import java.util.List; + + +@ResourceDef(name = MyEpisodeOfCareFHIR.FHIR_RESOURCE_NAME, id = MyEpisodeOfCareFHIR.FHIR_PROFILE_NAME, profile = MyEpisodeOfCareFHIR.FHIR_PROFILE_URI) +public class MyEpisodeOfCareFHIR extends EpisodeOfCare { + + public static final String FHIR_RESOURCE_NAME = "EpisodeOfCare"; + public static final String FHIR_PROFILE_NAME = "MyEpisodeOfCare"; + public static final String FHIR_PROFILE_URI = "http://myfhir.dk/p/MyEpisodeOfCare"; + /** + * dischargeTo (extension) + */ + @Child(name = FIELD_DISCHARGETO, min = 0, max = 1, type = {StringType.class}) + @Description(shortDefinition = "", formalDefinition = "Discharge to") + @Extension(url = EXTURL_DISCHARGETO, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.StringType ourDischargeTo; + public static final String EXTURL_DISCHARGETO = "http://myfhir.dk/x/MyEpisodeOfCare-discharge-to"; + public static final String FIELD_DISCHARGETO = "dischargeTo"; + /** + * dischargeDisposition (extension) + */ + @Child(name = FIELD_DISCHARGEDISPOSITION, min = 0, max = 1, type = {Coding.class}) + @Description(shortDefinition = "", formalDefinition = "Category or kind of location after discharge.") + @Extension(url = EXTURL_DISCHARGEDISPOSITION, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.Coding ourDischargeDisposition; + public static final String EXTURL_DISCHARGEDISPOSITION = "http://myfhir.dk/x/MyEpisodeOfCare-discharge-disposition"; + public static final String FIELD_DISCHARGEDISPOSITION = "dischargeDisposition"; + /** + * previous (extension) + */ + @Child(name = FIELD_PREVIOUS, min = 0, max = 1, type = {_PreviousComponent.class}) + @Description(shortDefinition = "", formalDefinition = "Previous reference between episode of care.") + @Extension(url = EXTURL_PREVIOUS, definedLocally = false, isModifier = false) + protected _PreviousComponent ourPrevious; + public static final String EXTURL_PREVIOUS = "http://myfhir.dk/x/MyEpisodeOfCare-previous"; + public static final String FIELD_PREVIOUS = "previous"; + /** + * referralInformation (extension) + */ + @Child(name = FIELD_REFERRALINFORMATION, min = 1, max = 1, type = {_MyReferralInformationComponent.class}) + @Description(shortDefinition = "", formalDefinition = "Referral information related to this episode of care.") + @Extension(url = EXTURL_REFERRALINFORMATION, definedLocally = false, isModifier = false) + protected _MyReferralInformationComponent ourReferralInformation; + public static final String EXTURL_REFERRALINFORMATION = "http://myfhir.dk/x/MyEpisodeOfCare-referral-information"; + public static final String FIELD_REFERRALINFORMATION = "referralInformation"; + /** + * eventMarker (extension) + */ + @Child(name = FIELD_EVENTMARKER, min = 0, max = Child.MAX_UNLIMITED, type = {_EventMarkerComponent.class}) + @Description(shortDefinition = "", formalDefinition = "Marks specific times on an episode of care with clinical or administrative relevance.") + @Extension(url = EXTURL_EVENTMARKER, definedLocally = false, isModifier = false) + protected List<_EventMarkerComponent> ourEventMarker; + public static final String EXTURL_EVENTMARKER = "http://myfhir.dk/x/MyEpisodeOfCare-event-marker"; + public static final String FIELD_EVENTMARKER = "eventMarker"; + /** + * payor (extension) + */ + @Child(name = FIELD_PAYOR, min = 0, max = Child.MAX_UNLIMITED, type = {_PayorComponent.class}) + @Description(shortDefinition = "", formalDefinition = "Payor information for time periods") + @Extension(url = EXTURL_PAYOR, definedLocally = false, isModifier = false) + protected List<_PayorComponent> ourPayor; + public static final String EXTURL_PAYOR = "http://myfhir.dk/x/MyEpisodeOfCare-payor"; + public static final String FIELD_PAYOR = "payor"; + /** + * healthIssue (extension) + */ + @Child(name = FIELD_HEALTHISSUE, min = 0, max = 1, type = {Condition.class}) + @Description(shortDefinition = "", formalDefinition = "The health issue this episode of care is related to.") + @Extension(url = EXTURL_HEALTHISSUE, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.Reference ourHealthIssue; + public static final String EXTURL_HEALTHISSUE = "http://myfhir.dk/x/MyEpisodeOfCare-health-issue"; + public static final String FIELD_HEALTHISSUE = "healthIssue"; + /** + * identifier + */ + @Child(name = FIELD_IDENTIFIER, min = 0, max = Child.MAX_UNLIMITED, order = Child.REPLACE_PARENT, type = {Identifier.class}) + @Description(shortDefinition = "Business Identifier(s) relevant for this EpisodeOfCare", formalDefinition = "Identifiers which the episode of care is known by.") + protected List ourIdentifier; + public static final String FIELD_IDENTIFIER = "identifier"; + /** + * status + */ + @Child(name = FIELD_STATUS, min = 1, max = 1, order = Child.REPLACE_PARENT, modifier = true, summary = true, type = {CodeType.class}) + @Description(shortDefinition = "planned | waitlist | active | onhold | finished | cancelled | entered-in-error", formalDefinition = "Status of the episode of care.") + protected org.hl7.fhir.dstu3.model.Enumeration ourStatus; + public static final String FIELD_STATUS = "status"; + /** + * patient + */ + @Child(name = FIELD_PATIENT, min = 1, max = 1, order = Child.REPLACE_PARENT, summary = true, type = {Patient.class}) + @Description(shortDefinition = "The patient who is the focus of this episode of care", formalDefinition = "The patient who is the subject of this episode of care.") + protected org.hl7.fhir.dstu3.model.Reference ourPatient; + public static final String FIELD_PATIENT = "patient"; + /** + * managingOrganization + */ + @Child(name = FIELD_MANAGINGORGANIZATION, min = 0, max = 1, order = Child.REPLACE_PARENT, summary = true, type = {Organization.class}) + @Description(shortDefinition = "Organization that assumes care", formalDefinition = "The organization that assumes care.") + protected org.hl7.fhir.dstu3.model.Reference ourManagingOrganization; + public static final String FIELD_MANAGINGORGANIZATION = "managingOrganization"; + /** + * period + */ + @Child(name = FIELD_PERIOD, min = 1, max = 1, order = Child.REPLACE_PARENT, summary = true, type = {Period.class}) + @Description(shortDefinition = "Interval during responsibility is assumed", formalDefinition = "The start and end time of the episode of care.") + protected Period ourPeriod; + public static final String FIELD_PERIOD = "period"; + /** + * careManager + */ + @Child(name = FIELD_CAREMANAGER, min = 0, max = 1, order = Child.REPLACE_PARENT, type = {Practitioner.class}) + @Description(shortDefinition = "Care manager/care co-ordinator for the patient", formalDefinition = "Care manager") + protected org.hl7.fhir.dstu3.model.Reference ourCareManager; + public static final String FIELD_CAREMANAGER = "careManager"; + /** + * + */ + @Child(name = "statusHistory", min = 0, max = 0, order = Child.REPLACE_PARENT) + @Deprecated + protected ca.uhn.fhir.model.api.IElement ourStatusHistory; + /** + * + */ + @Child(name = "type", min = 0, max = 0, order = Child.REPLACE_PARENT) + @Deprecated + protected ca.uhn.fhir.model.api.IElement ourType; + /** + * + */ + @Child(name = "diagnosis", min = 0, max = 0, order = Child.REPLACE_PARENT) + @Deprecated + protected ca.uhn.fhir.model.api.IElement ourDiagnosis; + /** + * + */ + @Child(name = "referralRequest", min = 0, max = 0, order = Child.REPLACE_PARENT) + @Deprecated + protected ca.uhn.fhir.model.api.IElement ourReferralRequest; + /** + * + */ + @Child(name = "team", min = 0, max = 0, order = Child.REPLACE_PARENT) + @Deprecated + protected ca.uhn.fhir.model.api.IElement ourTeam; + /** + * + */ + @Child(name = "account", min = 0, max = 0, order = Child.REPLACE_PARENT) + @Deprecated + protected ca.uhn.fhir.model.api.IElement ourAccount; + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(ourDischargeTo, ourDischargeDisposition, ourPrevious, ourReferralInformation, ourEventMarker, ourPayor, ourHealthIssue, ourIdentifier, ourStatus, ourPatient, ourManagingOrganization, ourPeriod, ourCareManager); + } + + @Override + public MyEpisodeOfCareFHIR copy() { + MyEpisodeOfCareFHIR dst = new MyEpisodeOfCareFHIR(); + copyValues(dst); + dst.ourDischargeTo = ourDischargeTo == null ? null : ourDischargeTo.copy(); + dst.ourDischargeDisposition = ourDischargeDisposition == null ? null : ourDischargeDisposition.copy(); + dst.ourPrevious = ourPrevious == null ? null : ourPrevious.copy(); + dst.ourReferralInformation = ourReferralInformation == null ? null : ourReferralInformation.copy(); + if (ourEventMarker != null) { + dst.ourEventMarker = new ArrayList<_EventMarkerComponent>(); + for (_EventMarkerComponent i : ourEventMarker) { + dst.ourEventMarker.add(i.copy()); + } + } + if (ourPayor != null) { + dst.ourPayor = new ArrayList<_PayorComponent>(); + for (_PayorComponent i : ourPayor) { + dst.ourPayor.add(i.copy()); + } + } + dst.ourHealthIssue = ourHealthIssue == null ? null : ourHealthIssue.copy(); + if (ourIdentifier != null) { + dst.ourIdentifier = new ArrayList(); + for (org.hl7.fhir.dstu3.model.Identifier i : ourIdentifier) { + dst.ourIdentifier.add(i.copy()); + } + } + dst.ourStatus = ourStatus == null ? null : ourStatus.copy(); + dst.ourPatient = ourPatient == null ? null : ourPatient.copy(); + dst.ourManagingOrganization = ourManagingOrganization == null ? null : ourManagingOrganization.copy(); + dst.ourPeriod = ourPeriod == null ? null : ourPeriod.copy(); + dst.ourCareManager = ourCareManager == null ? null : ourCareManager.copy(); + return dst; + } + + @Override + public boolean equalsDeep(Base other) { + if (this == other) { + return true; + } + if (!super.equalsDeep(other)) { + return false; + } + if (!(other instanceof MyEpisodeOfCareFHIR)) { + return false; + } + MyEpisodeOfCareFHIR that = (MyEpisodeOfCareFHIR) other; + return compareDeep(ourDischargeTo, that.ourDischargeTo, true) && compareDeep(ourDischargeDisposition, that.ourDischargeDisposition, true) && compareDeep(ourPrevious, that.ourPrevious, true) && compareDeep(ourReferralInformation, that.ourReferralInformation, true) + && compareDeep(ourEventMarker, that.ourEventMarker, true) && compareDeep(ourPayor, that.ourPayor, true) && compareDeep(ourHealthIssue, that.ourHealthIssue, true) && compareDeep(ourIdentifier, that.ourIdentifier, true) && compareDeep(ourStatus, that.ourStatus, true) + && compareDeep(ourPatient, that.ourPatient, true) && compareDeep(ourManagingOrganization, that.ourManagingOrganization, true) && compareDeep(ourPeriod, that.ourPeriod, true) && compareDeep(ourCareManager, that.ourCareManager, true); + } + + @Override + public boolean equalsShallow(Base other) { + if (this == other) { + return true; + } + if (!super.equalsShallow(other)) { + return false; + } + if (!(other instanceof MyEpisodeOfCareFHIR)) { + return false; + } + MyEpisodeOfCareFHIR that = (MyEpisodeOfCareFHIR) other; + return compareValues(ourDischargeTo, that.ourDischargeTo, true) && compareValues(ourStatus, that.ourStatus, true); + } + + public org.hl7.fhir.dstu3.model.StringType _getDischargeTo() { + if (ourDischargeTo == null) + ourDischargeTo = new org.hl7.fhir.dstu3.model.StringType(); + return ourDischargeTo; + } + + public MyEpisodeOfCareFHIR _setDischargeTo(org.hl7.fhir.dstu3.model.StringType theValue) { + ourDischargeTo = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Coding _getDischargeDisposition() { + if (ourDischargeDisposition == null) + ourDischargeDisposition = new org.hl7.fhir.dstu3.model.Coding(); + return ourDischargeDisposition; + } + + public MyEpisodeOfCareFHIR _setDischargeDisposition(org.hl7.fhir.dstu3.model.Coding theValue) { + ourDischargeDisposition = theValue; + return this; + } + + public _PreviousComponent _getPrevious() { + if (ourPrevious == null) + ourPrevious = new _PreviousComponent(); + return ourPrevious; + } + + public MyEpisodeOfCareFHIR _setPrevious(_PreviousComponent theValue) { + ourPrevious = theValue; + return this; + } + + public _MyReferralInformationComponent _getReferralInformation() { + if (ourReferralInformation == null) + ourReferralInformation = new _MyReferralInformationComponent(); + return ourReferralInformation; + } + + public MyEpisodeOfCareFHIR _setReferralInformation(_MyReferralInformationComponent theValue) { + ourReferralInformation = theValue; + return this; + } + + public List<_EventMarkerComponent> _getEventMarker() { + if (ourEventMarker == null) + ourEventMarker = new ArrayList<>(); + return ourEventMarker; + } + + public MyEpisodeOfCareFHIR _setEventMarker(List<_EventMarkerComponent> theValue) { + ourEventMarker = theValue; + return this; + } + + public List<_PayorComponent> _getPayor() { + if (ourPayor == null) + ourPayor = new ArrayList<>(); + return ourPayor; + } + + public MyEpisodeOfCareFHIR _setPayor(List<_PayorComponent> theValue) { + ourPayor = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Reference _getHealthIssue() { + if (ourHealthIssue == null) + ourHealthIssue = new org.hl7.fhir.dstu3.model.Reference(); + return ourHealthIssue; + } + + public MyEpisodeOfCareFHIR _setHealthIssue(org.hl7.fhir.dstu3.model.Reference theValue) { + ourHealthIssue = theValue; + return this; + } + + public List _getIdentifier() { + if (ourIdentifier == null) + ourIdentifier = new ArrayList<>(); + return ourIdentifier; + } + + public MyEpisodeOfCareFHIR _setIdentifier(List theValue) { + ourIdentifier = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Enumeration _getStatus() { + if (ourStatus == null) + ourStatus = new org.hl7.fhir.dstu3.model.Enumeration(new EpisodeOfCareStatusEnumFactory()); + return ourStatus; + } + + public MyEpisodeOfCareFHIR _setStatus(org.hl7.fhir.dstu3.model.Enumeration theValue) { + ourStatus = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Reference _getPatient() { + if (ourPatient == null) + ourPatient = new org.hl7.fhir.dstu3.model.Reference(); + return ourPatient; + } + + public MyEpisodeOfCareFHIR _setPatient(org.hl7.fhir.dstu3.model.Reference theValue) { + ourPatient = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Reference _getManagingOrganization() { + if (ourManagingOrganization == null) + ourManagingOrganization = new org.hl7.fhir.dstu3.model.Reference(); + return ourManagingOrganization; + } + + public MyEpisodeOfCareFHIR _setManagingOrganization(org.hl7.fhir.dstu3.model.Reference theValue) { + ourManagingOrganization = theValue; + return this; + } + + public Period _getPeriod() { + if (ourPeriod == null) + ourPeriod = new Period(); + return ourPeriod; + } + + public MyEpisodeOfCareFHIR _setPeriod(Period theValue) { + ourPeriod = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Reference _getCareManager() { + if (ourCareManager == null) + ourCareManager = new org.hl7.fhir.dstu3.model.Reference(); + return ourCareManager; + } + + public MyEpisodeOfCareFHIR _setCareManager(org.hl7.fhir.dstu3.model.Reference theValue) { + ourCareManager = theValue; + return this; + } + + @Override + @Deprecated + public void listChildren(List p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasAccount() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasCareManager() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasDiagnosis() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasIdentifier() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasManagingOrganization() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasPatient() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasPeriod() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasReferralRequest() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasStatus() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasStatusElement() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasStatusHistory() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasTeam() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public boolean hasType() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public String fhirType() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public String[] getTypesForProperty(int p0, String p1) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getAccount() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getAccountTarget() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getDiagnosis() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getIdentifier() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getReferralRequest() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getReferralRequestTarget() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getStatusHistory() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getTeam() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getTeamTarget() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public List getType() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Account addAccountTarget() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Base addChild(String p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Base makeProperty(int p0, String p1) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Base setProperty(int p0, String p1, Base p2) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Base setProperty(String p0, Base p1) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Base[] getProperty(int p0, String p1, boolean p2) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public CareTeam addTeamTarget() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public CodeableConcept addType() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public CodeableConcept getTypeFirstRep() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Enumeration getStatusElement() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare addAccount(Reference p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare addDiagnosis(DiagnosisComponent p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare addIdentifier(Identifier p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare addReferralRequest(Reference p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare addStatusHistory(EpisodeOfCareStatusHistoryComponent p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare addTeam(Reference p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare addType(CodeableConcept p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setAccount(List p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setCareManager(Reference p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setCareManagerTarget(Practitioner p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setDiagnosis(List p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setIdentifier(List p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setManagingOrganization(Reference p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setManagingOrganizationTarget(Organization p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setPatient(Reference p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setPatientTarget(Patient p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setPeriod(Period p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setReferralRequest(List p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setStatus(EpisodeOfCareStatus p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setStatusElement(Enumeration p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setStatusHistory(List p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setTeam(List p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCare setType(List p0) { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public DiagnosisComponent addDiagnosis() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public DiagnosisComponent getDiagnosisFirstRep() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCareStatus getStatus() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCareStatusHistoryComponent addStatusHistory() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public EpisodeOfCareStatusHistoryComponent getStatusHistoryFirstRep() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Identifier addIdentifier() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Identifier getIdentifierFirstRep() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Organization getManagingOrganizationTarget() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Patient getPatientTarget() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Period getPeriod() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Practitioner getCareManagerTarget() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Reference addAccount() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Reference addReferralRequest() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Reference addTeam() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Reference getAccountFirstRep() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Reference getCareManager() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Reference getManagingOrganization() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Reference getPatient() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Reference getReferralRequestFirstRep() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public Reference getTeamFirstRep() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public ReferralRequest addReferralRequestTarget() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + @Deprecated + public ResourceType getResourceType() { + throw new UnsupportedOperationException("Deprecated method"); + } + + @Override + protected MyEpisodeOfCareFHIR typedCopy() { + return copy(); + } +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/ResourceWithExtensionsDstu3A.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/ResourceWithExtensionsDstu3A.java index c07606fec4c..eca045b7b46 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/ResourceWithExtensionsDstu3A.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/ResourceWithExtensionsDstu3A.java @@ -1,47 +1,40 @@ package ca.uhn.fhir.context; +import ca.uhn.fhir.model.api.annotation.*; +import ca.uhn.fhir.model.api.annotation.Extension; +import org.hl7.fhir.dstu3.model.*; + import java.util.List; -import org.hl7.fhir.dstu3.model.*; - -import ca.uhn.fhir.model.api.BaseIdentifiableElement; -import ca.uhn.fhir.model.api.IElement; -import ca.uhn.fhir.model.api.IExtension; -import ca.uhn.fhir.model.api.annotation.Block; -import ca.uhn.fhir.model.api.annotation.Child; -import ca.uhn.fhir.model.api.annotation.Description; -import ca.uhn.fhir.model.api.annotation.Extension; -import ca.uhn.fhir.model.api.annotation.ResourceDef; - -@ResourceDef(name = "ResourceWithExtensionsA", id="0001") +@ResourceDef(name = "ResourceWithExtensionsA", id = "0001") public class ResourceWithExtensionsDstu3A extends DomainResource { /* * NB: several unit tests depend on the structure here - * so check the unit tests immediately after any changes + * so check the unit tests immediately after any changes */ - + private static final long serialVersionUID = 1L; @Child(name = "foo1", type = StringType.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) - @Extension(url = "http://foo/#f1", definedLocally=true, isModifier=false) + @Extension(url = "http://foo/#f1", definedLocally = true, isModifier = false) private List myFoo1; @Child(name = "foo2", type = StringType.class, order = 1, min = 0, max = 1) - @Extension(url = "http://foo/#f2", definedLocally=true, isModifier=true) + @Extension(url = "http://foo/#f2", definedLocally = true, isModifier = true) private StringType myFoo2; @Child(name = "bar1", type = Bar1.class, order = 2, min = 1, max = Child.MAX_UNLIMITED) - @Extension(url = "http://bar/#b1", definedLocally=true, isModifier=false) + @Extension(url = "http://bar/#b1", definedLocally = true, isModifier = false) private List myBar1; - + @Child(name = "bar2", type = Bar1.class, order = 3, min = 1, max = Child.MAX_UNLIMITED) - @Extension(url = "http://bar/#b2", definedLocally=true, isModifier=false) + @Extension(url = "http://bar/#b2", definedLocally = true, isModifier = false) private Bar1 myBar2; - - @Child(name="baz", type = CodeableConcept.class, order = 4) - @Extension(url= "http://baz/#baz", definedLocally=true, isModifier=false) - @Description(shortDefinition = "Contains a codeable concept") + + @Child(name = "baz", type = CodeableConcept.class, order = 4) + @Extension(url = "http://baz/#baz", definedLocally = true, isModifier = false) + @Description(shortDefinition = "Contains a codeable concept") private CodeableConcept myBaz; @Child(name = "identifier", type = Identifier.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) @@ -51,143 +44,55 @@ public class ResourceWithExtensionsDstu3A extends DomainResource { return myBar1; } - public Bar1 getBar2() { - return myBar2; - } - - public List getFoo1() { - return myFoo1; - } - - public StringType getFoo2() { - return myFoo2; - } - - public CodeableConcept getBaz() { return myBaz; } - - public List getIdentifier() { - return myIdentifier; - } - public void setBar1(List theBar1) { myBar1 = theBar1; } + public Bar1 getBar2() { + return myBar2; + } + public void setBar2(Bar1 theBar2) { myBar2 = theBar2; } + public List getFoo1() { + return myFoo1; + } + public void setFoo1(List theFoo1) { myFoo1 = theFoo1; } + public StringType getFoo2() { + return myFoo2; + } + public void setFoo2(StringType theFoo2) { myFoo2 = theFoo2; } - public void setBaz(CodeableConcept myBaz) { this.myBaz = myBaz; } + public CodeableConcept getBaz() { + return myBaz; + } + + public void setBaz(CodeableConcept myBaz) { + this.myBaz = myBaz; + } + + public List getIdentifier() { + return myIdentifier; + } public void setIdentifier(List theValue) { myIdentifier = theValue; } - @Block(name = "Bar1") - public static class Bar1 extends BaseIdentifiableElement implements IExtension { - - public Bar1() { - super(); - } - - @Child(name = "bar11", type = DateType.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) - @Extension(url = "http://bar/#b1/1", definedLocally=true, isModifier=false) - private List myBar11; - - @Child(name = "bar12", type = DateType.class, order = 1, min = 0, max = Child.MAX_UNLIMITED) - @Extension(url = "http://bar/#b1/2", definedLocally=true, isModifier=false) - private List myBar12; - - private IdType myId; - - @Override - public boolean isEmpty() { - return false; // not implemented - } - - @Override - public List getAllPopulatedChildElementsOfType(Class theType) { - return ca.uhn.fhir.util.ElementUtil.allPopulatedChildElements(theType ); // not implemented - } - - - public List getBar11() { - return myBar11; - } - - public List getBar12() { - return myBar12; - } - - public void setBar11(List theBar11) { - myBar11 = theBar11; - } - - public void setBar12(List theBar12) { - myBar12 = theBar12; - } - - - - } - - @Block(name = "Bar2") - public static class Bar2 extends BaseIdentifiableElement implements IExtension { - - @Child(name = "bar121", type = DateType.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) - @Extension(url = "http://bar/#b1/2/1", definedLocally=true, isModifier=false) - private List myBar121; - - @Child(name = "bar122", type = DateType.class, order = 1, min = 0, max = Child.MAX_UNLIMITED) - @Extension(url = "http://bar/#b1/2/2", definedLocally=true, isModifier=false) - private List myBar122; - - @Override - public boolean isEmpty() { - return false; // not implemented - } - - @Override - public List getAllPopulatedChildElementsOfType(Class theType) { - return ca.uhn.fhir.util.ElementUtil.allPopulatedChildElements(theType ); // not implemented - } - - - public List getBar121() { - return myBar121; - } - - public List getBar122() { - return myBar122; - } - - public void setBar121(List theBar121) { - myBar121 = theBar121; - } - - public void setBar122(List theBar122) { - myBar122 = theBar122; - } - - - - } - @Override public boolean isEmpty() { return false; // not implemented } - - @Override public String getId() { return null; @@ -238,5 +143,90 @@ public class ResourceWithExtensionsDstu3A extends DomainResource { return null; } + @Block(name = "Bar1") + public static class Bar1 extends BackboneElement { -} \ No newline at end of file + @Child(name = "bar11", type = DateType.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b1/1", definedLocally = true, isModifier = false) + private List myBar11; + @Child(name = "bar12", type = DateType.class, order = 1, min = 0, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b1/2", definedLocally = true, isModifier = false) + private List myBar12; + private IdType myId; + + public Bar1() { + super(); + } + + @Override + public BackboneElement copy() { + return this; + } + + @Override + public boolean isEmpty() { + return false; // not implemented + } + + public List getBar11() { + return myBar11; + } + + public void setBar11(List theBar11) { + myBar11 = theBar11; + } + + public List getBar12() { + return myBar12; + } + + public void setBar12(List theBar12) { + myBar12 = theBar12; + } + + + } + + @Block(name = "Bar2") + public static class Bar2 extends BackboneElement { + + @Child(name = "bar121", type = DateType.class, order = 0, min = 0, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b1/2/1", definedLocally = true, isModifier = false) + private List myBar121; + + @Child(name = "bar122", type = DateType.class, order = 1, min = 0, max = Child.MAX_UNLIMITED) + @Extension(url = "http://bar/#b1/2/2", definedLocally = true, isModifier = false) + private List myBar122; + + @Override + public BackboneElement copy() { + return this; + } + + @Override + public boolean isEmpty() { + return false; // not implemented + } + + + public List getBar121() { + return myBar121; + } + + public void setBar121(List theBar121) { + myBar121 = theBar121; + } + + public List getBar122() { + return myBar122; + } + + public void setBar122(List theBar122) { + myBar122 = theBar122; + } + + + } + + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_EventMarkerComponent.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_EventMarkerComponent.java new file mode 100644 index 00000000000..a78d6c3e85f --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_EventMarkerComponent.java @@ -0,0 +1,101 @@ +package ca.uhn.fhir.context; + +import ca.uhn.fhir.model.api.annotation.Block; +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.util.ElementUtil; +import org.hl7.fhir.dstu3.model.BackboneElement; +import org.hl7.fhir.dstu3.model.Base; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.DateTimeType; + +@Block +public class _EventMarkerComponent extends BackboneElement implements org.hl7.fhir.instance.model.api.IBaseBackboneElement { + + /** + * eventType (extension) + */ + @Child(name = FIELD_EVENTTYPE, min = 1, max = 1, type = {Coding.class}) + @Description(shortDefinition = "", formalDefinition = "The type of event marker on an episode of care.") + @Extension(url = EXTURL_EVENTTYPE, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.Coding ourEventType; + public static final String EXTURL_EVENTTYPE = "http://myfhir.dk/x/MyEpisodeOfCare-event-marker/eventType"; + public static final String FIELD_EVENTTYPE = "eventType"; + /** + * eventTimestamp (extension) + */ + @Child(name = FIELD_EVENTTIMESTAMP, min = 1, max = 1, type = {DateTimeType.class}) + @Description(shortDefinition = "", formalDefinition = "Time in which the event marker was created.") + @Extension(url = EXTURL_EVENTTIMESTAMP, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.DateTimeType ourEventTimestamp; + public static final String EXTURL_EVENTTIMESTAMP = "http://myfhir.dk/x/MyEpisodeOfCare-event-marker/eventTimestamp"; + public static final String FIELD_EVENTTIMESTAMP = "eventTimestamp"; + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(ourEventType, ourEventTimestamp); + } + + @Override + public _EventMarkerComponent copy() { + _EventMarkerComponent dst = new _EventMarkerComponent(); + copyValues(dst); + dst.ourEventType = ourEventType == null ? null : ourEventType.copy(); + dst.ourEventTimestamp = ourEventTimestamp == null ? null : ourEventTimestamp.copy(); + return dst; + } + + @Override + public boolean equalsDeep(Base other) { + if (this == other) { + return true; + } + if (!super.equalsDeep(other)) { + return false; + } + if (!(other instanceof _EventMarkerComponent)) { + return false; + } + _EventMarkerComponent that = (_EventMarkerComponent) other; + return compareDeep(ourEventType, that.ourEventType, true) && compareDeep(ourEventTimestamp, that.ourEventTimestamp, true); + } + + @Override + public boolean equalsShallow(Base other) { + if (this == other) { + return true; + } + if (!super.equalsShallow(other)) { + return false; + } + if (!(other instanceof _EventMarkerComponent)) { + return false; + } + _EventMarkerComponent that = (_EventMarkerComponent) other; + return compareValues(ourEventTimestamp, that.ourEventTimestamp, true); + } + + public org.hl7.fhir.dstu3.model.Coding _getEventType() { + if (ourEventType == null) + ourEventType = new org.hl7.fhir.dstu3.model.Coding(); + return ourEventType; + } + + public _EventMarkerComponent _setEventType(org.hl7.fhir.dstu3.model.Coding theValue) { + ourEventType = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.DateTimeType _getEventTimestamp() { + if (ourEventTimestamp == null) + ourEventTimestamp = new org.hl7.fhir.dstu3.model.DateTimeType(); + return ourEventTimestamp; + } + + public _EventMarkerComponent _setEventTimestamp(org.hl7.fhir.dstu3.model.DateTimeType theValue) { + ourEventTimestamp = theValue; + return this; + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_MyReferralInformationComponent.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_MyReferralInformationComponent.java new file mode 100644 index 00000000000..50fea802e9d --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_MyReferralInformationComponent.java @@ -0,0 +1,162 @@ +package ca.uhn.fhir.context; + +import ca.uhn.fhir.model.api.annotation.Block; +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.util.ElementUtil; +import org.hl7.fhir.dstu3.model.*; + +@Block +public class _MyReferralInformationComponent extends BackboneElement implements org.hl7.fhir.instance.model.api.IBaseBackboneElement { + + /** + * referralType (extension) + */ + @Child(name = FIELD_REFERRALTYPE, min = 1, max = 1, type = {Coding.class}) + @Description(shortDefinition = "", formalDefinition = "Referral type in referral info.") + @Extension(url = EXTURL_REFERRALTYPE, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.Coding ourReferralType; + public static final String EXTURL_REFERRALTYPE = "http://myfhir.dk/x/MyReferralInformation/referralType"; + public static final String FIELD_REFERRALTYPE = "referralType"; + /** + * referringOrganisation (extension) + */ + @Child(name = FIELD_REFERRINGORGANISATION, min = 0, max = 1, type = {Organization.class}) + @Description(shortDefinition = "", formalDefinition = "The organization which the referral originates from.") + @Extension(url = EXTURL_REFERRINGORGANISATION, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.Reference ourReferringOrganisation; + public static final String EXTURL_REFERRINGORGANISATION = "http://myfhir.dk/x/MyReferralInformation/referringOrganisation"; + public static final String FIELD_REFERRINGORGANISATION = "referringOrganisation"; + /** + * freeChoice (extension) + */ + @Child(name = FIELD_FREECHOICE, min = 1, max = 1, type = {Coding.class}) + @Description(shortDefinition = "", formalDefinition = "Type of free choice in relation to the referral decision.") + @Extension(url = EXTURL_FREECHOICE, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.Coding ourFreeChoice; + public static final String EXTURL_FREECHOICE = "http://myfhir.dk/x/MyReferralInformation/freeChoice"; + public static final String FIELD_FREECHOICE = "freeChoice"; + /** + * received (extension) + */ + @Child(name = FIELD_RECEIVED, min = 1, max = 1, type = {DateTimeType.class}) + @Description(shortDefinition = "", formalDefinition = "Time in which the referral was received.") + @Extension(url = EXTURL_RECEIVED, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.DateTimeType ourReceived; + public static final String EXTURL_RECEIVED = "http://myfhir.dk/x/MyReferralInformation/received"; + public static final String FIELD_RECEIVED = "received"; + /** + * referrer (extension) + */ + @Child(name = FIELD_REFERRER, min = 0, max = 1, type = {_MyReferrerComponent.class}) + @Description(shortDefinition = "", formalDefinition = "Referring organization, doctor or other.") + @Extension(url = EXTURL_REFERRER, definedLocally = false, isModifier = false) + protected _MyReferrerComponent ourReferrer; + public static final String EXTURL_REFERRER = "http://myfhir.dk/x/MyReferralInformation-referrer"; + public static final String FIELD_REFERRER = "referrer"; + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(ourReferralType, ourReferringOrganisation, ourFreeChoice, ourReceived, ourReferrer); + } + + @Override + public _MyReferralInformationComponent copy() { + _MyReferralInformationComponent dst = new _MyReferralInformationComponent(); + copyValues(dst); + dst.ourReferralType = ourReferralType == null ? null : ourReferralType.copy(); + dst.ourReferringOrganisation = ourReferringOrganisation == null ? null : ourReferringOrganisation.copy(); + dst.ourFreeChoice = ourFreeChoice == null ? null : ourFreeChoice.copy(); + dst.ourReceived = ourReceived == null ? null : ourReceived.copy(); + dst.ourReferrer = ourReferrer == null ? null : ourReferrer.copy(); + return dst; + } + + @Override + public boolean equalsDeep(Base other) { + if (this == other) { + return true; + } + if (!super.equalsDeep(other)) { + return false; + } + if (!(other instanceof _MyReferralInformationComponent)) { + return false; + } + _MyReferralInformationComponent that = (_MyReferralInformationComponent) other; + return compareDeep(ourReferralType, that.ourReferralType, true) && compareDeep(ourReferringOrganisation, that.ourReferringOrganisation, true) && compareDeep(ourFreeChoice, that.ourFreeChoice, true) && compareDeep(ourReceived, that.ourReceived, true) + && compareDeep(ourReferrer, that.ourReferrer, true); + } + + @Override + public boolean equalsShallow(Base other) { + if (this == other) { + return true; + } + if (!super.equalsShallow(other)) { + return false; + } + if (!(other instanceof _MyReferralInformationComponent)) { + return false; + } + _MyReferralInformationComponent that = (_MyReferralInformationComponent) other; + return compareValues(ourReceived, that.ourReceived, true); + } + + public org.hl7.fhir.dstu3.model.Coding _getReferralType() { + if (ourReferralType == null) + ourReferralType = new org.hl7.fhir.dstu3.model.Coding(); + return ourReferralType; + } + + public _MyReferralInformationComponent _setReferralType(org.hl7.fhir.dstu3.model.Coding theValue) { + ourReferralType = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Reference _getReferringOrganisation() { + if (ourReferringOrganisation == null) + ourReferringOrganisation = new org.hl7.fhir.dstu3.model.Reference(); + return ourReferringOrganisation; + } + + public _MyReferralInformationComponent _setReferringOrganisation(org.hl7.fhir.dstu3.model.Reference theValue) { + ourReferringOrganisation = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Coding _getFreeChoice() { + if (ourFreeChoice == null) + ourFreeChoice = new org.hl7.fhir.dstu3.model.Coding(); + return ourFreeChoice; + } + + public _MyReferralInformationComponent _setFreeChoice(org.hl7.fhir.dstu3.model.Coding theValue) { + ourFreeChoice = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.DateTimeType _getReceived() { + if (ourReceived == null) + ourReceived = new org.hl7.fhir.dstu3.model.DateTimeType(); + return ourReceived; + } + + public _MyReferralInformationComponent _setReceived(org.hl7.fhir.dstu3.model.DateTimeType theValue) { + ourReceived = theValue; + return this; + } + + public _MyReferrerComponent _getReferrer() { + if (ourReferrer == null) + ourReferrer = new _MyReferrerComponent(); + return ourReferrer; + } + + public _MyReferralInformationComponent _setReferrer(_MyReferrerComponent theValue) { + ourReferrer = theValue; + return this; + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_MyReferrerComponent.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_MyReferrerComponent.java new file mode 100644 index 00000000000..7969362186b --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_MyReferrerComponent.java @@ -0,0 +1,140 @@ +package ca.uhn.fhir.context; + +import ca.uhn.fhir.model.api.annotation.Block; +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.util.ElementUtil; +import org.hl7.fhir.dstu3.model.*; + +@Block +public class _MyReferrerComponent extends BackboneElement implements org.hl7.fhir.instance.model.api.IBaseBackboneElement { + + /** + * referrerType (extension) + */ + @Child(name = FIELD_REFERRERTYPE, min = 1, max = 1, type = {StringType.class}) + @Description(shortDefinition = "", formalDefinition = "Type of the selected referrer") + @Extension(url = EXTURL_REFERRERTYPE, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.StringType ourReferrerType; + public static final String EXTURL_REFERRERTYPE = "http://myfhir.dk/x/MyReferrer/referrerType"; + public static final String FIELD_REFERRERTYPE = "referrerType"; + /** + * hospitalReferrer (extension) + */ + @Child(name = FIELD_HOSPITALREFERRER, min = 0, max = 1, type = {CodeType.class}) + @Description(shortDefinition = "", formalDefinition = "Hospital department reference.") + @Extension(url = EXTURL_HOSPITALREFERRER, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.CodeType ourHospitalReferrer; + public static final String EXTURL_HOSPITALREFERRER = "http://myfhir.dk/x/MyReferrer/hospitalReferrer"; + public static final String FIELD_HOSPITALREFERRER = "hospitalReferrer"; + /** + * doctorReferrer (extension) + */ + @Child(name = FIELD_DOCTORREFERRER, min = 0, max = 1, type = {Practitioner.class}) + @Description(shortDefinition = "", formalDefinition = "Reference to a medical practitioner or a specialist doctor.") + @Extension(url = EXTURL_DOCTORREFERRER, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.Reference ourDoctorReferrer; + public static final String EXTURL_DOCTORREFERRER = "http://myfhir.dk/x/MyReferrer/doctorReferrer"; + public static final String FIELD_DOCTORREFERRER = "doctorReferrer"; + /** + * otherReferrer (extension) + */ + @Child(name = FIELD_OTHERREFERRER, min = 0, max = 1, type = {_OtherReferrerComponent.class}) + @Description(shortDefinition = "", formalDefinition = "Name, address and phone number of the referrer.") + @Extension(url = EXTURL_OTHERREFERRER, definedLocally = false, isModifier = false) + protected _OtherReferrerComponent ourOtherReferrer; + public static final String EXTURL_OTHERREFERRER = "http://myfhir.dk/x/MyReferrer/otherReferrer"; + public static final String FIELD_OTHERREFERRER = "otherReferrer"; + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(ourReferrerType, ourHospitalReferrer, ourDoctorReferrer, ourOtherReferrer); + } + + @Override + public _MyReferrerComponent copy() { + _MyReferrerComponent dst = new _MyReferrerComponent(); + copyValues(dst); + dst.ourReferrerType = ourReferrerType == null ? null : ourReferrerType.copy(); + dst.ourHospitalReferrer = ourHospitalReferrer == null ? null : ourHospitalReferrer.copy(); + dst.ourDoctorReferrer = ourDoctorReferrer == null ? null : ourDoctorReferrer.copy(); + dst.ourOtherReferrer = ourOtherReferrer == null ? null : ourOtherReferrer.copy(); + return dst; + } + + @Override + public boolean equalsDeep(Base other) { + if (this == other) { + return true; + } + if (!super.equalsDeep(other)) { + return false; + } + if (!(other instanceof _MyReferrerComponent)) { + return false; + } + _MyReferrerComponent that = (_MyReferrerComponent) other; + return compareDeep(ourReferrerType, that.ourReferrerType, true) && compareDeep(ourHospitalReferrer, that.ourHospitalReferrer, true) && compareDeep(ourDoctorReferrer, that.ourDoctorReferrer, true) && compareDeep(ourOtherReferrer, that.ourOtherReferrer, true); + } + + @Override + public boolean equalsShallow(Base other) { + if (this == other) { + return true; + } + if (!super.equalsShallow(other)) { + return false; + } + if (!(other instanceof _MyReferrerComponent)) { + return false; + } + _MyReferrerComponent that = (_MyReferrerComponent) other; + return compareValues(ourReferrerType, that.ourReferrerType, true) && compareValues(ourHospitalReferrer, that.ourHospitalReferrer, true); + } + + public org.hl7.fhir.dstu3.model.StringType _getReferrerType() { + if (ourReferrerType == null) + ourReferrerType = new org.hl7.fhir.dstu3.model.StringType(); + return ourReferrerType; + } + + public _MyReferrerComponent _setReferrerType(org.hl7.fhir.dstu3.model.StringType theValue) { + ourReferrerType = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.CodeType _getHospitalReferrer() { + if (ourHospitalReferrer == null) + ourHospitalReferrer = new org.hl7.fhir.dstu3.model.CodeType(); + return ourHospitalReferrer; + } + + public _MyReferrerComponent _setHospitalReferrer(org.hl7.fhir.dstu3.model.CodeType theValue) { + ourHospitalReferrer = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Reference _getDoctorReferrer() { + if (ourDoctorReferrer == null) + ourDoctorReferrer = new org.hl7.fhir.dstu3.model.Reference(); + return ourDoctorReferrer; + } + + public _MyReferrerComponent _setDoctorReferrer(org.hl7.fhir.dstu3.model.Reference theValue) { + ourDoctorReferrer = theValue; + return this; + } + + public _OtherReferrerComponent _getOtherReferrer() { + if (ourOtherReferrer == null) + ourOtherReferrer = new _OtherReferrerComponent(); + return ourOtherReferrer; + } + + public _MyReferrerComponent _setOtherReferrer(_OtherReferrerComponent theValue) { + ourOtherReferrer = theValue; + return this; + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_OtherReferrerComponent.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_OtherReferrerComponent.java new file mode 100644 index 00000000000..00087dfdaa6 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_OtherReferrerComponent.java @@ -0,0 +1,122 @@ +package ca.uhn.fhir.context; + +import ca.uhn.fhir.model.api.annotation.Block; +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.util.ElementUtil; +import org.hl7.fhir.dstu3.model.Address; +import org.hl7.fhir.dstu3.model.BackboneElement; +import org.hl7.fhir.dstu3.model.Base; +import org.hl7.fhir.dstu3.model.StringType; + +@Block +public class _OtherReferrerComponent extends BackboneElement implements org.hl7.fhir.instance.model.api.IBaseBackboneElement { + + /** + * name (extension) + */ + @Child(name = FIELD_NAME, min = 0, max = 1, type = {StringType.class}) + @Description(shortDefinition = "", formalDefinition = "Name of the referrer") + @Extension(url = EXTURL_NAME, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.StringType ourName; + public static final String EXTURL_NAME = "http://myfhir.dk/x/MyReferrer/otherReferrer/name"; + public static final String FIELD_NAME = "name"; + /** + * address (extension) + */ + @Child(name = FIELD_ADDRESS, min = 0, max = 1, type = {Address.class}) + @Description(shortDefinition = "", formalDefinition = "Address of the referrer") + @Extension(url = EXTURL_ADDRESS, definedLocally = false, isModifier = false) + protected Address ourAddress; + public static final String EXTURL_ADDRESS = "http://myfhir.dk/x/MyReferrer/otherReferrer/address"; + public static final String FIELD_ADDRESS = "address"; + /** + * phoneNumber (extension) + */ + @Child(name = FIELD_PHONENUMBER, min = 0, max = 1, type = {StringType.class}) + @Description(shortDefinition = "", formalDefinition = "Phone number of the referrer") + @Extension(url = EXTURL_PHONENUMBER, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.StringType ourPhoneNumber; + public static final String EXTURL_PHONENUMBER = "http://myfhir.dk/x/MyReferrer/otherReferrer/phoneNumber"; + public static final String FIELD_PHONENUMBER = "phoneNumber"; + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(ourName, ourAddress, ourPhoneNumber); + } + + @Override + public _OtherReferrerComponent copy() { + _OtherReferrerComponent dst = new _OtherReferrerComponent(); + copyValues(dst); + dst.ourName = ourName == null ? null : ourName.copy(); + dst.ourAddress = ourAddress == null ? null : ourAddress.copy(); + dst.ourPhoneNumber = ourPhoneNumber == null ? null : ourPhoneNumber.copy(); + return dst; + } + + @Override + public boolean equalsDeep(Base other) { + if (this == other) { + return true; + } + if (!super.equalsDeep(other)) { + return false; + } + if (!(other instanceof _OtherReferrerComponent)) { + return false; + } + _OtherReferrerComponent that = (_OtherReferrerComponent) other; + return compareDeep(ourName, that.ourName, true) && compareDeep(ourAddress, that.ourAddress, true) && compareDeep(ourPhoneNumber, that.ourPhoneNumber, true); + } + + @Override + public boolean equalsShallow(Base other) { + if (this == other) { + return true; + } + if (!super.equalsShallow(other)) { + return false; + } + if (!(other instanceof _OtherReferrerComponent)) { + return false; + } + _OtherReferrerComponent that = (_OtherReferrerComponent) other; + return compareValues(ourName, that.ourName, true) && compareValues(ourPhoneNumber, that.ourPhoneNumber, true); + } + + public org.hl7.fhir.dstu3.model.StringType _getName() { + if (ourName == null) + ourName = new org.hl7.fhir.dstu3.model.StringType(); + return ourName; + } + + public _OtherReferrerComponent _setName(org.hl7.fhir.dstu3.model.StringType theValue) { + ourName = theValue; + return this; + } + + public Address _getAddress() { + if (ourAddress == null) + ourAddress = new Address(); + return ourAddress; + } + + public _OtherReferrerComponent _setAddress(Address theValue) { + ourAddress = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.StringType _getPhoneNumber() { + if (ourPhoneNumber == null) + ourPhoneNumber = new org.hl7.fhir.dstu3.model.StringType(); + return ourPhoneNumber; + } + + public _OtherReferrerComponent _setPhoneNumber(org.hl7.fhir.dstu3.model.StringType theValue) { + ourPhoneNumber = theValue; + return this; + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_PayorComponent.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_PayorComponent.java new file mode 100644 index 00000000000..963d32b016c --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_PayorComponent.java @@ -0,0 +1,119 @@ +package ca.uhn.fhir.context; + +import ca.uhn.fhir.model.api.annotation.Block; +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.util.ElementUtil; +import org.hl7.fhir.dstu3.model.*; + +@Block +public class _PayorComponent extends BackboneElement implements org.hl7.fhir.instance.model.api.IBaseBackboneElement { + + /** + * payorCode (extension) + */ + @Child(name = FIELD_PAYORCODE, min = 1, max = 1, type = {Coding.class}) + @Description(shortDefinition = "", formalDefinition = "The payor code for the duration") + @Extension(url = EXTURL_PAYORCODE, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.Coding ourPayorCode; + public static final String EXTURL_PAYORCODE = "http://myfhir.dk/x/MyEpisodeOfCare-payor/payorCode"; + public static final String FIELD_PAYORCODE = "payorCode"; + /** + * period (extension) + */ + @Child(name = FIELD_PERIOD, min = 1, max = 1, type = {Period.class}) + @Description(shortDefinition = "", formalDefinition = "The duration for the responsible payor.") + @Extension(url = EXTURL_PERIOD, definedLocally = false, isModifier = false) + protected Period ourPeriod; + public static final String EXTURL_PERIOD = "http://myfhir.dk/x/MyEpisodeOfCare-payor/period"; + public static final String FIELD_PERIOD = "period"; + /** + * userDefined (extension) + */ + @Child(name = FIELD_USERDEFINED, min = 1, max = 1, type = {BooleanType.class}) + @Description(shortDefinition = "", formalDefinition = "True if the payor information is defined by a user.") + @Extension(url = EXTURL_USERDEFINED, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.BooleanType ourUserDefined; + public static final String EXTURL_USERDEFINED = "http://myfhir.dk/x/MyEpisodeOfCare-payor/userDefined"; + public static final String FIELD_USERDEFINED = "userDefined"; + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(ourPayorCode, ourPeriod, ourUserDefined); + } + + @Override + public _PayorComponent copy() { + _PayorComponent dst = new _PayorComponent(); + copyValues(dst); + dst.ourPayorCode = ourPayorCode == null ? null : ourPayorCode.copy(); + dst.ourPeriod = ourPeriod == null ? null : ourPeriod.copy(); + dst.ourUserDefined = ourUserDefined == null ? null : ourUserDefined.copy(); + return dst; + } + + @Override + public boolean equalsDeep(Base other) { + if (this == other) { + return true; + } + if (!super.equalsDeep(other)) { + return false; + } + if (!(other instanceof _PayorComponent)) { + return false; + } + _PayorComponent that = (_PayorComponent) other; + return compareDeep(ourPayorCode, that.ourPayorCode, true) && compareDeep(ourPeriod, that.ourPeriod, true) && compareDeep(ourUserDefined, that.ourUserDefined, true); + } + + @Override + public boolean equalsShallow(Base other) { + if (this == other) { + return true; + } + if (!super.equalsShallow(other)) { + return false; + } + if (!(other instanceof _PayorComponent)) { + return false; + } + _PayorComponent that = (_PayorComponent) other; + return compareValues(ourUserDefined, that.ourUserDefined, true); + } + + public org.hl7.fhir.dstu3.model.Coding _getPayorCode() { + if (ourPayorCode == null) + ourPayorCode = new org.hl7.fhir.dstu3.model.Coding(); + return ourPayorCode; + } + + public _PayorComponent _setPayorCode(org.hl7.fhir.dstu3.model.Coding theValue) { + ourPayorCode = theValue; + return this; + } + + public Period _getPeriod() { + if (ourPeriod == null) + ourPeriod = new Period(); + return ourPeriod; + } + + public _PayorComponent _setPeriod(Period theValue) { + ourPeriod = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.BooleanType _getUserDefined() { + if (ourUserDefined == null) + ourUserDefined = new org.hl7.fhir.dstu3.model.BooleanType(); + return ourUserDefined; + } + + public _PayorComponent _setUserDefined(org.hl7.fhir.dstu3.model.BooleanType theValue) { + ourUserDefined = theValue; + return this; + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_PreviousComponent.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_PreviousComponent.java new file mode 100644 index 00000000000..b4693042b96 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/context/_PreviousComponent.java @@ -0,0 +1,101 @@ +package ca.uhn.fhir.context; + +import ca.uhn.fhir.model.api.annotation.Block; +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.util.ElementUtil; +import org.hl7.fhir.dstu3.model.BackboneElement; +import org.hl7.fhir.dstu3.model.Base; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.StringType; + +@Block +public class _PreviousComponent extends BackboneElement implements org.hl7.fhir.instance.model.api.IBaseBackboneElement { + + /** + * previousReference (extension) + */ + @Child(name = FIELD_PREVIOUSREFERENCE, min = 1, max = 1, type = {StringType.class}) + @Description(shortDefinition = "", formalDefinition = "Reference to the previous episode of care which may be external.") + @Extension(url = EXTURL_PREVIOUSREFERENCE, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.StringType ourPreviousReference; + public static final String EXTURL_PREVIOUSREFERENCE = "http://myfhir.dk/x/MyEpisodeOfCare-previous/previousReference"; + public static final String FIELD_PREVIOUSREFERENCE = "previousReference"; + /** + * referenceType (extension) + */ + @Child(name = FIELD_REFERENCETYPE, min = 1, max = 1, type = {Coding.class}) + @Description(shortDefinition = "", formalDefinition = "The type of reference to a previous episode of care.") + @Extension(url = EXTURL_REFERENCETYPE, definedLocally = false, isModifier = false) + protected org.hl7.fhir.dstu3.model.Coding ourReferenceType; + public static final String EXTURL_REFERENCETYPE = "http://myfhir.dk/x/MyEpisodeOfCare-previous/referenceType"; + public static final String FIELD_REFERENCETYPE = "referenceType"; + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(ourPreviousReference, ourReferenceType); + } + + @Override + public _PreviousComponent copy() { + _PreviousComponent dst = new _PreviousComponent(); + copyValues(dst); + dst.ourPreviousReference = ourPreviousReference == null ? null : ourPreviousReference.copy(); + dst.ourReferenceType = ourReferenceType == null ? null : ourReferenceType.copy(); + return dst; + } + + @Override + public boolean equalsDeep(Base other) { + if (this == other) { + return true; + } + if (!super.equalsDeep(other)) { + return false; + } + if (!(other instanceof _PreviousComponent)) { + return false; + } + _PreviousComponent that = (_PreviousComponent) other; + return compareDeep(ourPreviousReference, that.ourPreviousReference, true) && compareDeep(ourReferenceType, that.ourReferenceType, true); + } + + @Override + public boolean equalsShallow(Base other) { + if (this == other) { + return true; + } + if (!super.equalsShallow(other)) { + return false; + } + if (!(other instanceof _PreviousComponent)) { + return false; + } + _PreviousComponent that = (_PreviousComponent) other; + return compareValues(ourPreviousReference, that.ourPreviousReference, true); + } + + public org.hl7.fhir.dstu3.model.StringType _getPreviousReference() { + if (ourPreviousReference == null) + ourPreviousReference = new org.hl7.fhir.dstu3.model.StringType(); + return ourPreviousReference; + } + + public _PreviousComponent _setPreviousReference(org.hl7.fhir.dstu3.model.StringType theValue) { + ourPreviousReference = theValue; + return this; + } + + public org.hl7.fhir.dstu3.model.Coding _getReferenceType() { + if (ourReferenceType == null) + ourReferenceType = new org.hl7.fhir.dstu3.model.Coding(); + return ourReferenceType; + } + + public _PreviousComponent _setReferenceType(org.hl7.fhir.dstu3.model.Coding theValue) { + ourReferenceType = theValue; + return this; + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java index f4617ac9dcf..fa21ed916d0 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java @@ -370,46 +370,6 @@ public class GenericClientDstu3Test { } } - @Test - public void testAcceptHeaderWithEncodingSpecified() throws Exception { - final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; - - ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).then(new Answer() { - @Override - public InputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); - } - }); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - int idx = 0; - - client.setEncoding(EncodingEnum.JSON); - client.search() - .forResource("Device") - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Device?_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); - assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); - idx++; - - client.setEncoding(EncodingEnum.XML); - client.search() - .forResource("Device") - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); - assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); - idx++; - - } - @Test public void testBinaryCreateWithFhirContentType() throws Exception { IParser p = ourCtx.newXmlParser(); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java index e985aa452b4..b95044179e3 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java @@ -882,7 +882,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().withAnyName().onServer().andThen() + .allow("RULE 1").operation().withAnyName().onServer().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -908,7 +908,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").atAnyLevel().andThen() + .allow("RULE 1").operation().named("opName").atAnyLevel().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -964,7 +964,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opNameBadOp").atAnyLevel().andThen() + .allow("RULE 1").operation().named("opNameBadOp").atAnyLevel().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1020,7 +1020,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization() .build(); } }); @@ -1055,7 +1055,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andThen() + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().andThen() .allow("Rule 2").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen() .build(); } @@ -1093,7 +1093,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization() .build(); } }); @@ -1127,7 +1127,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onInstance(new IdType("http://example.com/Patient/1/_history/2")).andThen() + .allow("RULE 1").operation().named("opName").onInstance(new IdType("http://example.com/Patient/1/_history/2")).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1186,7 +1186,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onAnyInstance().andThen() + .allow("RULE 1").operation().named("opName").onAnyInstance().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1311,7 +1311,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onServer().andThen() + .allow("RULE 1").operation().named("opName").onServer().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1358,7 +1358,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onType(Patient.class).andThen() + .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1427,7 +1427,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onAnyType().andThen() + .allow("RULE 1").operation().named("opName").onAnyType().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1495,7 +1495,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onType(Organization.class).andThen() + .allow("RULE 1").operation().named("opName").onType(Organization.class).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1554,7 +1554,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onType(Patient.class).forTenantIds("TENANTA").andThen() + .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().forTenantIds("TENANTA").andThen() .build(); } }); @@ -1591,7 +1591,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("process-message").onType(MessageHeader.class).andThen() + .allow("RULE 1").operation().named("process-message").onType(MessageHeader.class).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1630,7 +1630,7 @@ public class AuthorizationInterceptorDstu3Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).withTester(new IAuthRuleTester() { + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().withTester(new IAuthRuleTester() { @Override public boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) { return theInputResourceId.getIdPart().equals("1"); diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 77eb6abbb94..296ee6048c5 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index 2453a4f67af..47f0aa435ee 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0 + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientR4Test.java index edd30b3bc63..4fae6470c88 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientR4Test.java @@ -52,6 +52,7 @@ import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; @@ -113,7 +114,7 @@ public class GenericClientR4Test { } @Test - public void testAcceptHeaderWithEncodingSpecified() throws Exception { + public void testAcceptHeaderCustom() throws Exception { final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); @@ -130,26 +131,41 @@ public class GenericClientR4Test { IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); int idx = 0; - client.setEncoding(EncodingEnum.JSON); - client.search() - .forResource("Device") - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Device?_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); - assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); - idx++; + // Custom accept value client.setEncoding(EncodingEnum.XML); client.search() .forResource("Device") .returnBundle(Bundle.class) + .accept("application/json") .execute(); - - assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); - assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); + assertEquals("http://example.com/fhir/Device", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + assertEquals("application/json", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); idx++; + // Empty accept value + + client.setEncoding(EncodingEnum.XML); + client.search() + .forResource("Device") + .returnBundle(Bundle.class) + .accept("") + .execute(); + assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); + idx++; + + // Null accept value + + client.setEncoding(EncodingEnum.XML); + client.search() + .forResource("Device") + .returnBundle(Bundle.class) + .accept(null) + .execute(); + assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); + idx++; } @Test @@ -217,7 +233,7 @@ public class GenericClientR4Test { IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); Binary bin = new Binary(); - bin.setContent(new byte[] {0, 1, 2, 3, 4}); + bin.setContent(new byte[]{0, 1, 2, 3, 4}); client.create().resource(bin).execute(); ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString()); @@ -227,7 +243,7 @@ public class GenericClientR4Test { assertEquals("application/fhir+xml;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); - assertArrayEquals(new byte[] {0, 1, 2, 3, 4}, ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)).getContent()); + assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)).getContent()); } @@ -306,7 +322,7 @@ public class GenericClientR4Test { when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer() { @Override public Header[] answer(InvocationOnMock theInvocation) { - return new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")}; + return new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")}; } }); when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); @@ -355,7 +371,7 @@ public class GenericClientR4Test { when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer() { @Override public Header[] answer(InvocationOnMock theInvocation) { - return new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")}; + return new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")}; } }); when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); @@ -880,6 +896,85 @@ public class GenericClientR4Test { assertEquals("true", ((IPrimitiveType) output.getParameterFirstRep().getValue()).getValueAsString()); } + /** + * Invoke an operation that returns HTML + * as a response (a HAPI FHIR server could accomplish this by returning + * a Binary resource) + */ + @Test + public void testOperationReturningArbitraryBinaryContentTextual() throws Exception { + IParser p = ourCtx.newXmlParser(); + + Parameters inputParams = new Parameters(); + inputParams.addParameter().setName("name").setValue(new BooleanType(true)); + + final String respString = "VALUE"; + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", "text/html")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(t -> new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[]{ + new BasicHeader("content-type", "text/html") + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + MethodOutcome result = client + .operation() + .onServer() + .named("opname") + .withParameters(inputParams) + .returnMethodOutcome() + .execute(); + + assertEquals(Binary.class, result.getResource().getClass()); + Binary binary = (Binary) result.getResource(); + assertEquals(respString, new String(binary.getContent(), Charsets.UTF_8)); + assertEquals("text/html", binary.getContentType()); + + assertEquals("http://example.com/fhir/$opname", capt.getAllValues().get(0).getURI().toASCIIString()); + } + + /** + * Invoke an operation that returns HTML + * as a response (a HAPI FHIR server could accomplish this by returning + * a Binary resource) + */ + @Test + public void testOperationReturningArbitraryBinaryContentNonTextual() throws Exception { + IParser p = ourCtx.newXmlParser(); + + Parameters inputParams = new Parameters(); + inputParams.addParameter().setName("name").setValue(new BooleanType(true)); + + final byte[] respBytes = new byte[]{0,1,2,3,4,5,6,7,8,9,100}; + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", "application/weird-numbers")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(t -> new ByteArrayInputStream(respBytes)); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[]{ + new BasicHeader("content-Type", "application/weird-numbers") + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + MethodOutcome result = client + .operation() + .onServer() + .named("opname") + .withParameters(inputParams) + .returnMethodOutcome() + .execute(); + + assertEquals(Binary.class, result.getResource().getClass()); + Binary binary = (Binary) result.getResource(); + assertEquals("application/weird-numbers", binary.getContentType()); + assertArrayEquals(respBytes, binary.getContent()); + assertEquals("http://example.com/fhir/$opname", capt.getAllValues().get(0).getURI().toASCIIString()); + } + @Test public void testOperationType() throws Exception { IParser p = ourCtx.newXmlParser(); @@ -2036,7 +2131,7 @@ public class GenericClientR4Test { when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer() { @Override public Header[] answer(InvocationOnMock theInvocation) { - return new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")}; + return new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")}; } }); when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); @@ -2084,7 +2179,7 @@ public class GenericClientR4Test { when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer() { @Override public Header[] answer(InvocationOnMock theInvocation) { - return new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")}; + return new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3")}; } }); when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); @@ -2137,7 +2232,7 @@ public class GenericClientR4Test { Binary bin = new Binary(); bin.setContentType("application/foo"); - bin.setContent(new byte[] {0, 1, 2, 3, 4}); + bin.setContent(new byte[]{0, 1, 2, 3, 4}); client.create().resource(bin).execute(); ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString()); @@ -2147,7 +2242,7 @@ public class GenericClientR4Test { assertEquals("application/foo", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue()); assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON_NON_LEGACY, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); - assertArrayEquals(new byte[] {0, 1, 2, 3, 4}, extractBodyAsByteArray(capt)); + assertArrayEquals(new byte[]{0, 1, 2, 3, 4}, extractBodyAsByteArray(capt)); } @@ -2215,7 +2310,7 @@ public class GenericClientR4Test { when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer() { @Override public Header[] answer(InvocationOnMock theInvocation) { - return new Header[] {}; + return new Header[]{}; } }); when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/DeleteConditionalR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/DeleteConditionalR4Test.java index af27c110792..52232ad16db 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/DeleteConditionalR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/DeleteConditionalR4Test.java @@ -1,10 +1,18 @@ package ca.uhn.fhir.rest.server; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.util.concurrent.TimeUnit; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.Delete; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.gclient.IDelete; +import ca.uhn.fhir.rest.gclient.IDeleteWithQuery; +import ca.uhn.fhir.rest.gclient.IDeleteWithQueryTyped; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; +import org.apache.commons.lang3.Validate; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; @@ -14,27 +22,28 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Patient; -import org.junit.*; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.annotation.*; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; -import ca.uhn.fhir.util.PortUtil; -import ca.uhn.fhir.util.TestUtil; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class DeleteConditionalR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DeleteConditionalR4Test.class); private static CloseableHttpClient ourClient; private static FhirContext ourCtx = FhirContext.forR4(); private static IGenericClient ourHapiClient; private static String ourLastConditionalUrl; private static IdType ourLastIdParam; private static boolean ourLastRequestWasDelete; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DeleteConditionalR4Test.class); private static int ourPort; private static Server ourServer; - + @Before public void before() { @@ -44,45 +53,46 @@ public class DeleteConditionalR4Test { } - @Test - public void testSearchStillWorks() throws Exception { + public void testSearchStillWorks() { Patient patient = new Patient(); patient.addIdentifier().setValue("002"); -// HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_pretty=true"); -// -// HttpResponse status = ourClient.execute(httpGet); -// -// String responseContent = IOUtils.toString(status.getEntity().getContent()); -// IOUtils.closeQuietly(status.getEntity().getContent()); -// -// ourLog.info("Response was:\n{}", responseContent); - - //@formatter:off ourHapiClient .delete() .resourceConditionalByType(Patient.class) - .where(Patient.IDENTIFIER.exactly().systemAndIdentifier("SOMESYS","SOMEID")) - .execute(); - //@formatter:on - + .where(Patient.IDENTIFIER.exactly().systemAndIdentifier("SOMESYS", "SOMEID")).execute(); + assertTrue(ourLastRequestWasDelete); assertEquals(null, ourLastIdParam); assertEquals("Patient?identifier=SOMESYS%7CSOMEID", ourLastConditionalUrl); } + public static class PatientProvider implements IResourceProvider { + + @Delete() + public MethodOutcome deletePatient(@IdParam IdType theIdParam, @ConditionalUrlParam String theConditional) { + ourLastRequestWasDelete = true; + ourLastConditionalUrl = theConditional; + ourLastIdParam = theIdParam; + return new MethodOutcome(new IdType("Patient/001/_history/002")); + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + } - @AfterClass public static void afterClassClearContext() throws Exception { ourServer.stop(); TestUtil.clearAllStaticFieldsForUnitTest(); } - - + @BeforeClass public static void beforeClass() throws Exception { ourPort = PortUtil.findFreePort(); @@ -107,22 +117,5 @@ public class DeleteConditionalR4Test { ourHapiClient = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort + "/"); ourHapiClient.registerInterceptor(new LoggingInterceptor()); } - - public static class PatientProvider implements IResourceProvider { - - @Delete() - public MethodOutcome deletePatient(@IdParam IdType theIdParam, @ConditionalUrlParam String theConditional) { - ourLastRequestWasDelete = true; - ourLastConditionalUrl = theConditional; - ourLastIdParam = theIdParam; - return new MethodOutcome(new IdType("Patient/001/_history/002")); - } - - @Override - public Class getResourceType() { - return Patient.class; - } - - } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java index 822edbdd426..b07bdfba4a2 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; @@ -41,9 +42,10 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; public class OperationServerR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerR4Test.class); + private static final String TEXT_HTML = "text/html"; private static CloseableHttpClient ourClient; private static FhirContext ourCtx; - private static IdType ourLastId; private static String ourLastMethod; private static StringType ourLastParam1; @@ -51,7 +53,6 @@ public class OperationServerR4Test { private static List ourLastParam3; private static MoneyQuantity ourLastParamMoney1; private static UnsignedIntType ourLastParamUnsignedInt1; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerR4Test.class); private static int ourPort; private static Server ourServer; private IGenericClient myFhirClient; @@ -84,21 +85,21 @@ public class OperationServerR4Test { List opNames = toOpNames(ops); assertThat(opNames, containsInRelativeOrder("OP_TYPE")); - + // OperationDefinition def = (OperationDefinition) ops.get(opNames.indexOf("OP_TYPE")).getDefinition().getResource(); OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId(ops.get(opNames.indexOf("OP_TYPE")).getDefinition()).execute(); assertEquals("OP_TYPE", def.getCode()); } - + /** * See #380 */ @Test public void testOperationDefinition() { OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient--OP_TYPE").execute(); - + ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(def)); - + // @OperationParam(name="PARAM1") StringType theParam1, // @OperationParam(name="PARAM2") Patient theParam2, // @OperationParam(name="PARAM3", min=2, max=5) List theParam3, @@ -109,22 +110,22 @@ public class OperationServerR4Test { assertEquals(OperationParameterUse.IN, def.getParameter().get(0).getUse()); assertEquals(0, def.getParameter().get(0).getMin()); assertEquals("1", def.getParameter().get(0).getMax()); - + assertEquals("PARAM2", def.getParameter().get(1).getName()); assertEquals(OperationParameterUse.IN, def.getParameter().get(1).getUse()); assertEquals(0, def.getParameter().get(1).getMin()); assertEquals("1", def.getParameter().get(1).getMax()); - + assertEquals("PARAM3", def.getParameter().get(2).getName()); assertEquals(OperationParameterUse.IN, def.getParameter().get(2).getUse()); assertEquals(2, def.getParameter().get(2).getMin()); assertEquals("5", def.getParameter().get(2).getMax()); - + assertEquals("PARAM4", def.getParameter().get(3).getName()); assertEquals(OperationParameterUse.IN, def.getParameter().get(3).getUse()); assertEquals(1, def.getParameter().get(3).getMin()); assertEquals("*", def.getParameter().get(3).getMax()); - + } private List toOpNames(List theOps) { @@ -137,7 +138,7 @@ public class OperationServerR4Test { @Test public void testInstanceEverythingGet() throws Exception { - + // Try with a GET HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$everything"); CloseableHttpResponse status = ourClient.execute(httpGet); @@ -149,9 +150,9 @@ public class OperationServerR4Test { assertEquals("instance $everything", ourLastMethod); assertThat(response, startsWith("", IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8)); + } } - @BeforeClass - public static void beforeClass() throws Exception { - ourCtx = FhirContext.forR4(); - ourPort = PortUtil.findFreePort(); - ourServer = new Server(ourPort); + @Test + public void testReturnBinaryWithAcceptHtml() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/$binaryop"); + httpGet.addHeader(Constants.HEADER_ACCEPT, TEXT_HTML); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("$binaryop", ourLastMethod); - ServletHandler proxyHandler = new ServletHandler(); - RestfulServer servlet = new RestfulServer(ourCtx); - - servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2)); - - servlet.setFhirContext(ourCtx); - servlet.setResourceProviders(new PatientProvider()); - servlet.setPlainProviders(new PlainProvider()); - ServletHolder servletHolder = new ServletHolder(servlet); - proxyHandler.addServletWithMapping(servletHolder, "/*"); - ourServer.setHandler(proxyHandler); - ourServer.start(); - - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); - HttpClientBuilder builder = HttpClientBuilder.create(); - builder.setConnectionManager(connectionManager); - ourClient = builder.build(); - - } - - - public static void main(String[] theValue) { - Parameters p = new Parameters(); - p.addParameter().setName("start").setValue(new DateTimeType("2001-01-02")); - p.addParameter().setName("end").setValue(new DateTimeType("2015-07-10")); - String inParamsStr = FhirContext.forDstu2().newXmlParser().encodeResourceToString(p); - ourLog.info(inParamsStr.replace("\"", "\\\"")); + assertEquals("text/html", status.getEntity().getContentType().getValue()); + assertEquals("TAGS", IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8)); + } } public static class PatientProvider implements IResourceProvider { @@ -612,12 +597,12 @@ public class OperationServerR4Test { } //@formatter:off - @Operation(name="$OP_INSTANCE") + @Operation(name = "$OP_INSTANCE") public Parameters opInstance( - @IdParam IdType theId, - @OperationParam(name="PARAM1") StringType theParam1, - @OperationParam(name="PARAM2") Patient theParam2 - ) { + @IdParam IdType theId, + @OperationParam(name = "PARAM1") StringType theParam1, + @OperationParam(name = "PARAM2") Patient theParam2 + ) { //@formatter:on ourLastMethod = "$OP_INSTANCE"; @@ -631,12 +616,12 @@ public class OperationServerR4Test { } //@formatter:off - @Operation(name="$OP_INSTANCE_OR_TYPE") + @Operation(name = "$OP_INSTANCE_OR_TYPE") public Parameters opInstanceOrType( - @IdParam(optional=true) IdType theId, - @OperationParam(name="PARAM1") StringType theParam1, - @OperationParam(name="PARAM2") Patient theParam2 - ) { + @IdParam(optional = true) IdType theId, + @OperationParam(name = "PARAM1") StringType theParam1, + @OperationParam(name = "PARAM2") Patient theParam2 + ) { //@formatter:on ourLastMethod = "$OP_INSTANCE_OR_TYPE"; @@ -650,10 +635,10 @@ public class OperationServerR4Test { } //@formatter:off - @Operation(name="$OP_PROFILE_DT2", idempotent=true) + @Operation(name = "$OP_PROFILE_DT2", idempotent = true) public Bundle opProfileType( - @OperationParam(name="PARAM1") MoneyQuantity theParam1 - ) { + @OperationParam(name = "PARAM1") MoneyQuantity theParam1 + ) { //@formatter:on ourLastMethod = "$OP_PROFILE_DT2"; @@ -665,10 +650,10 @@ public class OperationServerR4Test { } //@formatter:off - @Operation(name="$OP_PROFILE_DT", idempotent=true) + @Operation(name = "$OP_PROFILE_DT", idempotent = true) public Bundle opProfileType( - @OperationParam(name="PARAM1") UnsignedIntType theParam1 - ) { + @OperationParam(name = "PARAM1") UnsignedIntType theParam1 + ) { //@formatter:on ourLastMethod = "$OP_PROFILE_DT"; @@ -681,13 +666,13 @@ public class OperationServerR4Test { //@formatter:off @SuppressWarnings("unused") - @Operation(name="$OP_TYPE", idempotent=true) + @Operation(name = "$OP_TYPE", idempotent = true) public Parameters opType( - @OperationParam(name="PARAM1") StringType theParam1, - @OperationParam(name="PARAM2") Patient theParam2, - @OperationParam(name="PARAM3", min=2, max=5) List theParam3, - @OperationParam(name="PARAM4", min=1) List theParam4 - ) { + @OperationParam(name = "PARAM1") StringType theParam1, + @OperationParam(name = "PARAM2") Patient theParam2, + @OperationParam(name = "PARAM3", min = 2, max = 5) List theParam3, + @OperationParam(name = "PARAM4", min = 1) List theParam4 + ) { //@formatter:on ourLastMethod = "$OP_TYPE"; @@ -700,10 +685,10 @@ public class OperationServerR4Test { } //@formatter:off - @Operation(name="$OP_TYPE_ONLY_STRING", idempotent=true) + @Operation(name = "$OP_TYPE_ONLY_STRING", idempotent = true) public Parameters opTypeOnlyString( - @OperationParam(name="PARAM1") StringType theParam1 - ) { + @OperationParam(name = "PARAM1") StringType theParam1 + ) { //@formatter:on ourLastMethod = "$OP_TYPE"; @@ -715,11 +700,11 @@ public class OperationServerR4Test { } //@formatter:off - @Operation(name="$OP_TYPE_RET_BUNDLE") + @Operation(name = "$OP_TYPE_RET_BUNDLE") public Bundle opTypeRetBundle( - @OperationParam(name="PARAM1") StringType theParam1, - @OperationParam(name="PARAM2") Patient theParam2 - ) { + @OperationParam(name = "PARAM1") StringType theParam1, + @OperationParam(name = "PARAM2") Patient theParam2 + ) { //@formatter:on ourLastMethod = "$OP_TYPE_RET_BUNDLE"; @@ -731,7 +716,7 @@ public class OperationServerR4Test { return retVal; } - @Operation(name = "$everything", idempotent=true) + @Operation(name = "$everything", idempotent = true) public Bundle patientEverything(@IdParam IdType thePatientId) { ourLastMethod = "instance $everything"; ourLastId = thePatientId; @@ -754,27 +739,27 @@ public class OperationServerR4Test { public static class PlainProvider { //@formatter:off - @Operation(name="$OP_INSTANCE_BUNDLE_PROVIDER", idempotent=true) + @Operation(name = "$OP_INSTANCE_BUNDLE_PROVIDER", idempotent = true) public IBundleProvider opInstanceReturnsBundleProvider() { ourLastMethod = "$OP_INSTANCE_BUNDLE_PROVIDER"; List resources = new ArrayList(); - for (int i =0; i < 100;i++) { + for (int i = 0; i < 100; i++) { Patient p = new Patient(); p.setId("Patient/" + i); p.addName().setFamily("Patient " + i); resources.add(p); } - + return new SimpleBundleProvider(resources); } //@formatter:off - @Operation(name="$OP_SERVER") + @Operation(name = "$OP_SERVER") public Parameters opServer( - @OperationParam(name="PARAM1") StringType theParam1, - @OperationParam(name="PARAM2") Patient theParam2 - ) { + @OperationParam(name = "PARAM1") StringType theParam1, + @OperationParam(name = "PARAM2") Patient theParam2 + ) { //@formatter:on ourLastMethod = "$OP_SERVER"; @@ -787,10 +772,10 @@ public class OperationServerR4Test { } //@formatter:off - @Operation(name="$OP_SERVER_WITH_RAW_STRING") + @Operation(name = "$OP_SERVER_WITH_RAW_STRING") public Parameters opServer( - @OperationParam(name="PARAM1") String theParam1, - @OperationParam(name="PARAM2") Patient theParam2 + @OperationParam(name = "PARAM1") String theParam1, + @OperationParam(name = "PARAM2") Patient theParam2 ) { //@formatter:on @@ -803,13 +788,11 @@ public class OperationServerR4Test { return retVal; } - //@formatter:off - @Operation(name="$OP_SERVER_LIST_PARAM") + @Operation(name = "$OP_SERVER_LIST_PARAM") public Parameters opServerListParam( - @OperationParam(name="PARAM2") Patient theParam2, - @OperationParam(name="PARAM3") List theParam3 - ) { - //@formatter:on + @OperationParam(name = "PARAM2") Patient theParam2, + @OperationParam(name = "PARAM3") List theParam3 + ) { ourLastMethod = "$OP_SERVER_LIST_PARAM"; ourLastParam2 = theParam2; @@ -820,6 +803,60 @@ public class OperationServerR4Test { return retVal; } + @Operation(name = "$binaryop", idempotent = true) + public Binary binaryOp( + @OperationParam(name = "PARAM3", min = 0, max = 1) List theParam3 + ) { + + ourLastMethod = "$binaryop"; + ourLastParam3 = theParam3; + + Binary retVal = new Binary(); + retVal.setContentType(TEXT_HTML); + retVal.setContent("TAGS".getBytes(Charsets.UTF_8)); + return retVal; + } + + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourCtx = FhirContext.forR4(); + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + + servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2)); + + servlet.setFhirContext(ourCtx); + servlet.setResourceProviders(new PatientProvider()); + servlet.setPlainProviders(new PlainProvider()); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static void main(String[] theValue) { + Parameters p = new Parameters(); + p.addParameter().setName("start").setValue(new DateTimeType("2001-01-02")); + p.addParameter().setName("end").setValue(new DateTimeType("2015-07-10")); + String inParamsStr = FhirContext.forDstu2().newXmlParser().encodeResourceToString(p); + ourLog.info(inParamsStr.replace("\"", "\\\"")); } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorR4Test.java index e40dac56220..05591a172c9 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorR4Test.java @@ -15,9 +15,11 @@ import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.auth.*; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.util.UrlUtil; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; @@ -34,11 +36,10 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.*; +import org.springframework.util.Base64Utils; +import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -47,12 +48,10 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import static javax.print.DocFlavor.READER.TEXT_HTML; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class AuthorizationInterceptorR4Test { @@ -66,6 +65,7 @@ public class AuthorizationInterceptorR4Test { private static List ourReturn; private static Server ourServer; private static RestfulServer ourServlet; + private static String ourLastAcceptHeader; @Before public void before() { @@ -77,6 +77,7 @@ public class AuthorizationInterceptorR4Test { ourReturn = null; ourHitMethod = false; ourConditionalCreateId = "1123"; + ourLastAcceptHeader = null; } private Resource createCarePlan(Integer theId, String theSubjectId) { @@ -501,7 +502,6 @@ public class AuthorizationInterceptorR4Test { assertEquals(200, status.getStatusLine().getStatusCode()); - } @Test @@ -924,7 +924,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().withAnyName().onServer().andThen() + .allow("RULE 1").operation().withAnyName().onServer().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -950,7 +950,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").atAnyLevel().andThen() + .allow("RULE 1").operation().named("opName").atAnyLevel().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1006,7 +1006,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opNameBadOp").atAnyLevel().andThen() + .allow("RULE 1").operation().named("opNameBadOp").atAnyLevel().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1062,7 +1062,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization() .build(); } }); @@ -1092,12 +1092,13 @@ public class AuthorizationInterceptorR4Test { } @Test + @Ignore public void testOperationByInstanceOfTypeWithInvalidReturnValue() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andThen() + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().andThen() .allow("Rule 2").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen() .build(); } @@ -1135,7 +1136,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class) + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization() .build(); } }); @@ -1169,7 +1170,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onInstance(new IdType("http://example.com/Patient/1/_history/2")).andThen() + .allow("RULE 1").operation().named("opName").onInstance(new IdType("http://example.com/Patient/1/_history/2")).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1228,7 +1229,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onAnyInstance().andThen() + .allow("RULE 1").operation().named("opName").onAnyInstance().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1291,6 +1292,31 @@ public class AuthorizationInterceptorR4Test { } + @Test + public void testOperationInstanceLevelWithHtmlResponse() throws IOException { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("RULE 1").operation().named("binaryop").onInstancesOfType(Patient.class).andAllowAllResponses().andThen() + .build(); + } + }); + + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2/$binaryop"); + httpGet.addHeader(Constants.HEADER_ACCEPT, "text/html"); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + assertEquals(200, status.getStatusLine().getStatusCode()); + + assertEquals("text/html", status.getEntity().getContentType().getValue()); + assertEquals("TAGS", IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8)); + assertEquals("text/html", ourLastAcceptHeader); + } + + } + + @Test public void testOperationNotAllowedWithWritePermissiom() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -1348,12 +1374,12 @@ public class AuthorizationInterceptorR4Test { } @Test - public void testOperationServerLevel() throws Exception { + public void testOperationServerLevelAllowAllResponses() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onServer().andThen() + .allow("RULE 1").operation().named("opName").onServer().andAllowAllResponses().andThen() .build(); } }); @@ -1394,13 +1420,48 @@ public class AuthorizationInterceptorR4Test { assertFalse(ourHitMethod); } + @Test + public void testOperationServerLevelRequireResponseAuthorization() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("RULE 1").operation().named("opName").onServer().andRequireExplicitResponseAuthorization().andThen() + .allow().read().instance("Observation/10").andThen() + .build(); + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + // Server + ourHitMethod = false; + ourReturn = Collections.singletonList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Server + ourHitMethod = false; + ourReturn = Collections.singletonList(createObservation(99, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + } + @Test public void testOperationTypeLevel() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onType(Patient.class).andThen() + .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1469,7 +1530,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onAnyType().andThen() + .allow("RULE 1").operation().named("opName").onAnyType().andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1537,7 +1598,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onType(Organization.class).andThen() + .allow("RULE 1").operation().named("opName").onType(Organization.class).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1596,7 +1657,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onType(Patient.class).forTenantIds("TENANTA").andThen() + .allow("RULE 1").operation().named("opName").onType(Patient.class).andRequireExplicitResponseAuthorization().forTenantIds("TENANTA").andThen() .build(); } }); @@ -1633,7 +1694,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("RULE 1").operation().named("process-message").onType(MessageHeader.class).andThen() + .allow("RULE 1").operation().named("process-message").onType(MessageHeader.class).andRequireExplicitResponseAuthorization().andThen() .build(); } }); @@ -1672,7 +1733,7 @@ public class AuthorizationInterceptorR4Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() - .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).withTester(new IAuthRuleTester() { + .allow("Rule 1").operation().named("everything").onInstancesOfType(Patient.class).andRequireExplicitResponseAuthorization().withTester(new IAuthRuleTester() { @Override public boolean matches(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource) { return theInputResourceId.getIdPart().equals("1"); @@ -1818,6 +1879,57 @@ public class AuthorizationInterceptorR4Test { } + + @Test + public void testReadWithGraphQL() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("Rule 1").read().resourcesOfType(Patient.class).withAnyId().andThen() + .allow("Rule 2").graphQL().any().andThen() + .build(); + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + ourReturn = Collections.singletonList(createPatient(2)); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$graphql?query=" + UrlUtil.escapeUrlParam("{name}")); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + + } + + @Test + public void testReadWithGraphQLAllowAll() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .deny("Rule 1").read().resourcesOfType(Observation.class).withAnyId().andThen() + .allowAll() + .build(); + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + ourReturn = Collections.singletonList(createPatient(2)); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$graphql?query=" + UrlUtil.escapeUrlParam("{name}")); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + + } + @Test public void testReadByAnyIdWithTenantId() throws Exception { ourServlet.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy()); @@ -3032,45 +3144,6 @@ public class AuthorizationInterceptorR4Test { assertTrue(ourHitMethod); } - @AfterClass - public static void afterClassClearContext() throws Exception { - ourServer.stop(); - TestUtil.clearAllStaticFieldsForUnitTest(); - } - - @BeforeClass - public static void beforeClass() throws Exception { - - ourPort = PortUtil.findFreePort(); - ourServer = new Server(ourPort); - - DummyPatientResourceProvider patProvider = new DummyPatientResourceProvider(); - DummyObservationResourceProvider obsProv = new DummyObservationResourceProvider(); - DummyOrganizationResourceProvider orgProv = new DummyOrganizationResourceProvider(); - DummyEncounterResourceProvider encProv = new DummyEncounterResourceProvider(); - DummyCarePlanResourceProvider cpProv = new DummyCarePlanResourceProvider(); - DummyDiagnosticReportResourceProvider drProv = new DummyDiagnosticReportResourceProvider(); - DummyMessageHeaderResourceProvider mshProv = new DummyMessageHeaderResourceProvider(); - PlainProvider plainProvider = new PlainProvider(); - - ServletHandler proxyHandler = new ServletHandler(); - ourServlet = new RestfulServer(ourCtx); - ourServlet.setFhirContext(ourCtx); - ourServlet.setResourceProviders(patProvider, obsProv, encProv, cpProv, orgProv, drProv, mshProv); - ourServlet.setPlainProviders(plainProvider); - ourServlet.setPagingProvider(new FifoMemoryPagingProvider(100)); - ServletHolder servletHolder = new ServletHolder(ourServlet); - proxyHandler.addServletWithMapping(servletHolder, "/*"); - ourServer.setHandler(proxyHandler); - ourServer.start(); - - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); - HttpClientBuilder builder = HttpClientBuilder.create(); - builder.setConnectionManager(connectionManager); - ourClient = builder.build(); - - } - public static class DummyCarePlanResourceProvider implements IResourceProvider { @Override @@ -3141,7 +3214,7 @@ public class AuthorizationInterceptorR4Test { } @Operation(name = "process-message", idempotent = true) - public Parameters operation0(@OperationParam(name="content") Bundle theInput) { + public Parameters operation0(@OperationParam(name = "content") Bundle theInput) { ourHitMethod = true; return (Parameters) new Parameters().setId("1"); } @@ -3254,6 +3327,7 @@ public class AuthorizationInterceptorR4Test { @SuppressWarnings("unused") public static class DummyPatientResourceProvider implements IResourceProvider { + @Create() public MethodOutcome create(@ResourceParam Patient theResource, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails) { @@ -3293,6 +3367,26 @@ public class AuthorizationInterceptorR4Test { return retVal; } + @Operation(name = "$binaryop", idempotent = true) + public Binary binaryOp( + @IdParam IIdType theId, + @OperationParam(name = "PARAM3", min = 0, max = 1) List theParam3, + HttpServletRequest theServletRequest + ) { + ourLastAcceptHeader = theServletRequest.getHeader(ca.uhn.fhir.rest.api.Constants.HEADER_ACCEPT); + + Binary retVal = new Binary(); + if (ourLastAcceptHeader.contains("html")) { + retVal.setContentType("text/html"); + retVal.setContent("TAGS".getBytes(Charsets.UTF_8)); + } else { + retVal.setContentType("application/weird"); + retVal.setContent(new byte[]{0,0,1,1,2,2,3,3,0,0}); + } + return retVal; + } + + @Override public Class getResourceType() { return Patient.class; @@ -3421,6 +3515,49 @@ public class AuthorizationInterceptorR4Test { return (Bundle) ourReturn.get(0); } + @GraphQL + public String graphql(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQuery String theQuery) { + return "{}"; + } + } + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + DummyPatientResourceProvider patProvider = new DummyPatientResourceProvider(); + DummyObservationResourceProvider obsProv = new DummyObservationResourceProvider(); + DummyOrganizationResourceProvider orgProv = new DummyOrganizationResourceProvider(); + DummyEncounterResourceProvider encProv = new DummyEncounterResourceProvider(); + DummyCarePlanResourceProvider cpProv = new DummyCarePlanResourceProvider(); + DummyDiagnosticReportResourceProvider drProv = new DummyDiagnosticReportResourceProvider(); + DummyMessageHeaderResourceProvider mshProv = new DummyMessageHeaderResourceProvider(); + PlainProvider plainProvider = new PlainProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + ourServlet = new RestfulServer(ourCtx); + ourServlet.setFhirContext(ourCtx); + ourServlet.setResourceProviders(patProvider, obsProv, encProv, cpProv, orgProv, drProv, mshProv); + ourServlet.setPlainProviders(plainProvider); + ourServlet.setPagingProvider(new FifoMemoryPagingProvider(100)); + ServletHolder servletHolder = new ServletHolder(ourServlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java similarity index 77% rename from hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java index 8888c4180d7..d22aa144684 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java @@ -2,21 +2,7 @@ package ca.uhn.fhir.rest.server.interceptor; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.api.BundleInclusionRule; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.dstu2.composite.HumanNameDt; -import ca.uhn.fhir.model.dstu2.composite.IdentifierDt; -import ca.uhn.fhir.model.dstu2.resource.Binary; -import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; -import ca.uhn.fhir.model.dstu2.resource.OperationOutcome.Issue; -import ca.uhn.fhir.model.dstu2.resource.Organization; -import ca.uhn.fhir.model.dstu2.resource.Patient; -import ca.uhn.fhir.model.dstu2.valueset.IdentifierUseEnum; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.model.primitive.UriDt; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Read; -import ca.uhn.fhir.rest.annotation.RequiredParam; -import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -28,9 +14,9 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; +import com.google.common.base.Charsets; import com.helger.collection.iterate.ArrayEnumeration; import org.apache.commons.io.IOUtils; -import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; @@ -39,12 +25,12 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.*; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.springframework.web.cors.CorsConfiguration; import javax.servlet.http.HttpServletRequest; @@ -65,10 +51,8 @@ public class ResponseHighlightingInterceptorTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlightingInterceptorTest.class); private static ResponseHighlighterInterceptor ourInterceptor = new ResponseHighlighterInterceptor(); private static CloseableHttpClient ourClient; - private static FhirContext ourCtx = FhirContext.forDstu2(); + private static FhirContext ourCtx = FhirContext.forR4(); private static int ourPort; - - private static Server ourServer; private static RestfulServer ourServlet; @Before @@ -77,34 +61,68 @@ public class ResponseHighlightingInterceptorTest { ourInterceptor.setShowResponseHeaders(new ResponseHighlighterInterceptor().isShowResponseHeaders()); } + /** + * Return a Binary response type - Client accepts text/html but is not a browser + */ + @Test + public void testBinaryOperationHtmlResponseFromProvider() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/html/$binaryOp"); + httpGet.addHeader("Accept", "text/html"); + + CloseableHttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + status.close(); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("text/html", status.getFirstHeader("content-type").getValue()); + assertEquals("DATA", responseContent); + assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue()); + } + @Test public void testBinaryReadAcceptBrowser() throws Exception { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo"); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); httpGet.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("foo", status.getFirstHeader("content-type").getValue()); assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue()); assertArrayEquals(new byte[]{1, 2, 3, 4}, responseContent); } + /** + * Return a Binary response type - Client accepts text/html but is not a browser + */ + @Test + public void testBinaryReadHtmlResponseFromProvider() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/html"); + httpGet.addHeader("Accept", "text/html"); + + CloseableHttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + status.close(); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("text/html", status.getFirstHeader("content-type").getValue()); + assertEquals("DATA", responseContent); + assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue()); + } + @Test public void testBinaryReadAcceptFhirJson() throws Exception { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo"); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); httpGet.addHeader("Accept", Constants.CT_FHIR_JSON); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertNull(status.getFirstHeader("Content-Disposition")); - assertEquals("{\"resourceType\":\"Binary\",\"id\":\"1\",\"contentType\":\"foo\",\"content\":\"AQIDBA==\"}", responseContent); + assertEquals("{\"resourceType\":\"Binary\",\"id\":\"foo\",\"contentType\":\"foo\",\"data\":\"AQIDBA==\"}", responseContent); } @@ -112,9 +130,9 @@ public class ResponseHighlightingInterceptorTest { public void testBinaryReadAcceptMissing() throws Exception { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("foo", status.getFirstHeader("content-type").getValue()); assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue()); @@ -127,29 +145,19 @@ public class ResponseHighlightingInterceptorTest { ResponseHighlighterInterceptor ic = ourInterceptor; HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer>() { - @Override - public Enumeration answer(InvocationOnMock theInvocation) throws Throwable { - return new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9"); - } - }); - when(req.getHeader(Constants.HEADER_ORIGIN)).thenAnswer(new Answer() { - @Override - public String answer(InvocationOnMock theInvocation) throws Throwable { - return "http://example.com"; - } - }); + when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9")); + when(req.getHeader(Constants.HEADER_ORIGIN)).thenAnswer(theInvocation -> "http://example.com"); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter sw = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(sw)); Patient resource = new Patient(); - resource.addName().addFamily("FAMILY"); + resource.addName().setFamily("FAMILY"); ServletRequestDetails reqDetails = new TestServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); - HashMap params = new HashMap(); + HashMap params = new HashMap<>(); reqDetails.setParameters(params); reqDetails.setServer(new RestfulServer(ourCtx)); reqDetails.setServletRequest(req); @@ -164,11 +172,11 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/json"); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); - assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertEquals(Constants.CT_FHIR_JSON_NEW + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, not(containsString("html"))); } @@ -177,9 +185,9 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/json+fhir"); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, not(containsString("html"))); @@ -190,9 +198,9 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=" + UrlUtil.escapeUrlParam("application/json+fhir")); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, not(containsString("html"))); @@ -203,11 +211,11 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/xml"); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); + CloseableHttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); - assertEquals(Constants.CT_FHIR_XML + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertEquals(Constants.CT_FHIR_XML_NEW + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, not(containsString("html"))); } @@ -216,9 +224,9 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=application/xml+fhir"); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); + CloseableHttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_XML + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, not(containsString("html"))); @@ -229,9 +237,9 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=" + UrlUtil.escapeUrlParam("application/xml+fhir")); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); + CloseableHttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_XML + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, not(containsString("html"))); @@ -242,9 +250,9 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json"); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, containsString("html")); @@ -259,9 +267,9 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=" + UrlUtil.escapeUrlParam("html/json; fhirVersion=1.0")); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, containsString("html")); @@ -276,9 +284,9 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/xml"); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); + CloseableHttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, containsString("html")); @@ -291,11 +299,11 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=json"); httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); assertEquals(200, status.getStatusLine().getStatusCode()); - assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertEquals(Constants.CT_FHIR_JSON_NEW + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); assertThat(responseContent, not(containsString("html"))); } @@ -303,9 +311,9 @@ public class ResponseHighlightingInterceptorTest { public void testForceResponseTime() throws Exception { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); @@ -319,7 +327,7 @@ public class ResponseHighlightingInterceptorTest { httpGet.addHeader("Accept", "text/html"); CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); ourLog.info("Resp: {}", responseContent); assertEquals(404, status.getStatusLine().getStatusCode()); @@ -333,14 +341,14 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Foobar/123"); CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); ourLog.info("Resp: {}", responseContent); assertEquals(404, status.getStatusLine().getStatusCode()); assertThat(responseContent, not(stringContainsInOrder("OperationOutcome", "Unknown resource type 'Foobar' - Server knows how to handle"))); assertThat(responseContent, (stringContainsInOrder("Unknown resource type 'Foobar'"))); - assertThat(status.getFirstHeader("Content-Type").getValue(), containsString("application/xml+fhir")); + assertEquals(Constants.CT_FHIR_XML_NEW + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); } @@ -349,8 +357,8 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/"); httpGet.addHeader("Accept", "text/html"); CloseableHttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + status.close(); ourLog.info("Resp: {}", responseContent); assertEquals(400, status.getStatusLine().getStatusCode()); @@ -364,19 +372,14 @@ public class ResponseHighlightingInterceptorTest { ResponseHighlighterInterceptor ic = ourInterceptor; HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer>() { - @Override - public Enumeration answer(InvocationOnMock theInvocation) throws Throwable { - return new ArrayEnumeration("text/html,application/xhtml+xml,application/xml;q=0.9"); - } - }); + when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9")); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter sw = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(sw)); Patient resource = new Patient(); - resource.addName().addFamily("FAMILY"); + resource.addName().setFamily("FAMILY"); ServletRequestDetails reqDetails = new TestServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); @@ -387,7 +390,7 @@ public class ResponseHighlightingInterceptorTest { // reqDetails.setParameters(null); ResourceNotFoundException exception = new ResourceNotFoundException("Not found"); - exception.setOperationOutcome(new OperationOutcome().addIssue(new Issue().setDiagnostics("Hello"))); + exception.setOperationOutcome(new OperationOutcome().addIssue(new OperationOutcome.OperationOutcomeIssueComponent().setDiagnostics("Hello"))); assertFalse(ic.handleException(reqDetails, exception, req, resp)); @@ -404,23 +407,18 @@ public class ResponseHighlightingInterceptorTest { ResponseHighlighterInterceptor ic = ourInterceptor; HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer>() { - @Override - public Enumeration answer(InvocationOnMock theInvocation) throws Throwable { - return new ArrayEnumeration("application/xml+fhir"); - } - }); + when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("application/xml+fhir")); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter sw = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(sw)); Patient resource = new Patient(); - resource.addName().addFamily("FAMILY"); + resource.addName().setFamily("FAMILY"); ServletRequestDetails reqDetails = new TestServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); - HashMap params = new HashMap(); + HashMap params = new HashMap<>(); params.put(Constants.PARAM_FORMAT, new String[]{Constants.FORMAT_HTML}); reqDetails.setParameters(params); reqDetails.setServer(new RestfulServer(ourCtx)); @@ -438,23 +436,18 @@ public class ResponseHighlightingInterceptorTest { ResponseHighlighterInterceptor ic = ourInterceptor; HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer>() { - @Override - public Enumeration answer(InvocationOnMock theInvocation) throws Throwable { - return new ArrayEnumeration("application/xml+fhir"); - } - }); + when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("application/xml+fhir")); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter sw = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(sw)); Patient resource = new Patient(); - resource.addName().addFamily("FAMILY"); + resource.addName().setFamily("FAMILY"); ServletRequestDetails reqDetails = new TestServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); - HashMap params = new HashMap(); + HashMap params = new HashMap<>(); params.put(Constants.PARAM_FORMAT, new String[]{Constants.CT_HTML}); reqDetails.setParameters(params); reqDetails.setServer(new RestfulServer(ourCtx)); @@ -469,23 +462,18 @@ public class ResponseHighlightingInterceptorTest { ResponseHighlighterInterceptor ic = ourInterceptor; HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer>() { - @Override - public Enumeration answer(InvocationOnMock theInvocation) throws Throwable { - return new ArrayEnumeration("text/html,application/xhtml+xml,application/xml;q=0.9"); - } - }); + when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9")); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter sw = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(sw)); Patient resource = new Patient(); - resource.addName().addFamily("FAMILY"); + resource.addName().setFamily("FAMILY"); ServletRequestDetails reqDetails = new TestServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); - HashMap params = new HashMap(); + HashMap params = new HashMap<>(); params.put(Constants.PARAM_PRETTY, new String[]{Constants.PARAM_PRETTY_VALUE_TRUE}); params.put(Constants.PARAM_FORMAT, new String[]{Constants.CT_XML}); params.put(ResponseHighlighterInterceptor.PARAM_RAW, new String[]{ResponseHighlighterInterceptor.PARAM_RAW_TRUE}); @@ -503,23 +491,18 @@ public class ResponseHighlightingInterceptorTest { ResponseHighlighterInterceptor ic = ourInterceptor; HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer>() { - @Override - public Enumeration answer(InvocationOnMock theInvocation) throws Throwable { - return new ArrayEnumeration("text/html,application/xhtml+xml,application/xml;q=0.9"); - } - }); + when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9")); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter sw = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(sw)); Patient resource = new Patient(); - resource.addName().addFamily("FAMILY"); + resource.addName().setFamily("FAMILY"); ServletRequestDetails reqDetails = new TestServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); - reqDetails.setParameters(new HashMap()); + reqDetails.setParameters(new HashMap<>()); reqDetails.setServer(new RestfulServer(ourCtx)); reqDetails.setServletRequest(req); @@ -537,23 +520,18 @@ public class ResponseHighlightingInterceptorTest { ResponseHighlighterInterceptor ic = ourInterceptor; HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer>() { - @Override - public Enumeration answer(InvocationOnMock theInvocation) throws Throwable { - return new ArrayEnumeration("text/html,application/xhtml+xml,application/xml;q=0.9"); - } - }); + when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9")); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter sw = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(sw)); Patient resource = new Patient(); - resource.addName().addFamily("FAMILY"); + resource.addName().setFamily("FAMILY"); ServletRequestDetails reqDetails = new TestServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); - HashMap params = new HashMap(); + HashMap params = new HashMap<>(); params.put(Constants.PARAM_PRETTY, new String[]{Constants.PARAM_PRETTY_VALUE_TRUE}); reqDetails.setParameters(params); reqDetails.setServer(new RestfulServer(ourCtx)); @@ -576,23 +554,18 @@ public class ResponseHighlightingInterceptorTest { HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer>() { - @Override - public Enumeration answer(InvocationOnMock theInvocation) throws Throwable { - return new ArrayEnumeration("text/html,application/xhtml+xml,application/xml;q=0.9"); - } - }); + when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html,application/xhtml+xml,application/xml;q=0.9")); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter sw = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(sw)); Patient resource = new Patient(); - resource.addName().addFamily("FAMILY"); + resource.addName().setFamily("FAMILY"); ServletRequestDetails reqDetails = new TestServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); - reqDetails.setParameters(new HashMap()); + reqDetails.setParameters(new HashMap<>()); RestfulServer server = new RestfulServer(ourCtx); server.setDefaultResponseEncoding(EncodingEnum.JSON); reqDetails.setServer(server); @@ -611,23 +584,18 @@ public class ResponseHighlightingInterceptorTest { HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer>() { - @Override - public Enumeration answer(InvocationOnMock theInvocation) throws Throwable { - return new ArrayEnumeration("text/html;q=0.8,application/xhtml+xml,application/xml;q=0.9"); - } - }); + when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(theInvocation -> new ArrayEnumeration<>("text/html;q=0.8,application/xhtml+xml,application/xml;q=0.9")); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter sw = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(sw)); Patient resource = new Patient(); - resource.addName().addFamily("FAMILY"); + resource.addName().setFamily("FAMILY"); ServletRequestDetails reqDetails = new TestServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); - reqDetails.setParameters(new HashMap()); + reqDetails.setParameters(new HashMap<>()); RestfulServer server = new RestfulServer(ourCtx); server.setDefaultResponseEncoding(EncodingEnum.JSON); reqDetails.setServer(server); @@ -647,9 +615,9 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); httpGet.addHeader("Accept", "text/html"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); assertThat(responseContent, (stringContainsInOrder("", "
", "")));
@@ -665,9 +633,9 @@ public class ResponseHighlightingInterceptorTest {
 		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_pretty=false");
 		httpGet.addHeader("Accept", "text/html");
 
-		HttpResponse status = ourClient.execute(httpGet);
+		CloseableHttpResponse status = ourClient.execute(httpGet);
 		String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
-		IOUtils.closeQuietly(status.getEntity().getContent());
+		status.close();
 		ourLog.info(responseContent);
 		assertEquals(200, status.getStatusLine().getStatusCode());
 		assertThat(responseContent, not(stringContainsInOrder("", "
", "\n", "
"))); @@ -683,9 +651,9 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_pretty=true"); httpGet.addHeader("Accept", "text/html"); - HttpResponse status = ourClient.execute(httpGet); + CloseableHttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); + status.close(); ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); assertThat(responseContent, (stringContainsInOrder("", "
", "")));
@@ -697,7 +665,7 @@ public class ResponseHighlightingInterceptorTest {
 		httpGet.addHeader("Accept", "html");
 		CloseableHttpResponse status = ourClient.execute(httpGet);
 		String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
-		IOUtils.closeQuietly(status.getEntity().getContent());
+		status.close();
 
 		ourLog.info("Resp: {}", responseContent);
 		assertEquals(200, status.getStatusLine().getStatusCode());
@@ -711,9 +679,9 @@ public class ResponseHighlightingInterceptorTest {
 
 		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json");
 
-		HttpResponse status = ourClient.execute(httpGet);
+		CloseableHttpResponse status = ourClient.execute(httpGet);
 		String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
-		IOUtils.closeQuietly(status.getEntity().getContent());
+		status.close();
 		ourLog.info(responseContent);
 		assertEquals(200, status.getStatusLine().getStatusCode());
 		assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
@@ -728,9 +696,9 @@ public class ResponseHighlightingInterceptorTest {
 
 		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json");
 
-		HttpResponse status = ourClient.execute(httpGet);
+		CloseableHttpResponse status = ourClient.execute(httpGet);
 		String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
-		IOUtils.closeQuietly(status.getEntity().getContent());
+		status.close();
 		ourLog.info(responseContent);
 		assertEquals(200, status.getStatusLine().getStatusCode());
 		assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
@@ -745,9 +713,9 @@ public class ResponseHighlightingInterceptorTest {
 
 		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json");
 
-		HttpResponse status = ourClient.execute(httpGet);
+		CloseableHttpResponse status = ourClient.execute(httpGet);
 		String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
-		IOUtils.closeQuietly(status.getEntity().getContent());
+		status.close();
 		ourLog.info(responseContent);
 		assertEquals(200, status.getStatusLine().getStatusCode());
 		assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
@@ -761,9 +729,9 @@ public class ResponseHighlightingInterceptorTest {
 
 		HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json");
 
-		HttpResponse status = ourClient.execute(httpGet);
+		CloseableHttpResponse status = ourClient.execute(httpGet);
 		String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
-		IOUtils.closeQuietly(status.getEntity().getContent());
+		status.close();
 		ourLog.info(responseContent);
 		assertEquals(200, status.getStatusLine().getStatusCode());
 		assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
@@ -780,7 +748,7 @@ public class ResponseHighlightingInterceptorTest {
 	public static void beforeClass() throws Exception {
 		ourPort = PortUtil.findFreePort();
 		ourLog.info("Using port: {}", ourPort);
-		ourServer = new Server(ourPort);
+		Server ourServer = new Server(ourPort);
 
 		DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
 
@@ -830,16 +798,21 @@ public class ResponseHighlightingInterceptorTest {
 	public static class DummyBinaryResourceProvider implements IResourceProvider {
 
 		@Override
-		public Class getResourceType() {
+		public Class getResourceType() {
 			return Binary.class;
 		}
 
 		@Read
-		public Binary read(@IdParam IdDt theId) {
+		public Binary read(@IdParam IdType theId) {
 			Binary retVal = new Binary();
-			retVal.setId("1");
-			retVal.setContent(new byte[]{1, 2, 3, 4});
-			retVal.setContentType(theId.getIdPart());
+			retVal.setId(theId);
+			if (theId.getIdPart().equals("html")) {
+				retVal.setContent("DATA".getBytes(Charsets.UTF_8));
+				retVal.setContentType("text/html");
+			}else {
+				retVal.setContent(new byte[]{1, 2, 3, 4});
+				retVal.setContentType(theId.getIdPart());
+			}
 			return retVal;
 		}
 
@@ -859,13 +832,13 @@ public class ResponseHighlightingInterceptorTest {
 		private Patient createPatient1() {
 			Patient patient = new Patient();
 			patient.addIdentifier();
-			patient.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL);
-			patient.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns"));
+			patient.getIdentifier().get(0).setUse(Identifier.IdentifierUse.OFFICIAL);
+			patient.getIdentifier().get(0).setSystem("urn:hapitest:mrns");
 			patient.getIdentifier().get(0).setValue("00001");
 			patient.addName();
-			patient.getName().get(0).addFamily("Test");
+			patient.getName().get(0).setFamily("Test");
 			patient.getName().get(0).addGiven("PatientOne");
-			patient.getId().setValue("1");
+			patient.setId("1");
 			return patient;
 		}
 
@@ -889,22 +862,37 @@ public class ResponseHighlightingInterceptorTest {
 			return Collections.singletonList(p);
 		}
 
-		public Map getIdToPatient() {
-			Map idToPatient = new HashMap();
+		@Operation(name="binaryOp", idempotent = true)
+		public Binary binaryOp(@IdParam IdType theId) {
+			Binary retVal = new Binary();
+			retVal.setId(theId);
+			if (theId.getIdPart().equals("html")) {
+				retVal.setContent("DATA".getBytes(Charsets.UTF_8));
+				retVal.setContentType("text/html");
+			}else {
+				retVal.setContent(new byte[]{1, 2, 3, 4});
+				retVal.setContentType(theId.getIdPart());
+			}
+			return retVal;
+		}
+
+
+		Map getIdToPatient() {
+			Map idToPatient = new HashMap<>();
 			{
 				Patient patient = createPatient1();
 				idToPatient.put("1", patient);
 			}
 			{
 				Patient patient = new Patient();
-				patient.getIdentifier().add(new IdentifierDt());
-				patient.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL);
-				patient.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns"));
+				patient.getIdentifier().add(new Identifier());
+				patient.getIdentifier().get(0).setUse(Identifier.IdentifierUse.OFFICIAL);
+				patient.getIdentifier().get(0).setSystem("urn:hapitest:mrns");
 				patient.getIdentifier().get(0).setValue("00002");
-				patient.getName().add(new HumanNameDt());
-				patient.getName().get(0).addFamily("Test");
+				patient.getName().add(new HumanName());
+				patient.getName().get(0).setFamily("Test");
 				patient.getName().get(0).addGiven("PatientTwo");
-				patient.getId().setValue("2");
+				patient.setId("2");
 				idToPatient.put("2", patient);
 			}
 			return idToPatient;
@@ -917,10 +905,9 @@ public class ResponseHighlightingInterceptorTest {
 		 * @return The resource
 		 */
 		@Read()
-		public Patient getResourceById(@IdParam IdDt theId) {
+		public Patient getResourceById(@IdParam IdType theId) {
 			String key = theId.getIdPart();
-			Patient retVal = getIdToPatient().get(key);
-			return retVal;
+			return getIdToPatient().get(key);
 		}
 
 		/**
@@ -945,10 +932,10 @@ public class ResponseHighlightingInterceptorTest {
 		}
 
 		@Search(queryName = "searchWithWildcardRetVal")
-		public List searchWithWildcardRetVal() {
+		public List searchWithWildcardRetVal() {
 			Patient p = new Patient();
 			p.setId("1234");
-			p.addName().addFamily("searchWithWildcardRetVal");
+			p.addName().setFamily("searchWithWildcardRetVal");
 			return Collections.singletonList(p);
 		}
 
diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java
index eadbc300546..6a7cd0120e0 100644
--- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java
+++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java
@@ -3,6 +3,7 @@ package ca.uhn.fhir.rest.server.provider;
 import ca.uhn.fhir.context.FhirContext;
 import ca.uhn.fhir.rest.client.api.IGenericClient;
 import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
+import ca.uhn.fhir.rest.gclient.IDeleteTyped;
 import ca.uhn.fhir.rest.server.IResourceProvider;
 import ca.uhn.fhir.rest.server.RestfulServer;
 import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
@@ -89,7 +90,14 @@ public class HashMapResourceProviderTest {
 
 		assertEquals(0, myPatientResourceProvider.getCountDelete());
 
-		ourClient.delete().resourceById(id.toUnqualifiedVersionless()).execute();
+		IDeleteTyped iDeleteTyped = ourClient.delete().resourceById(id.toUnqualifiedVersionless());
+		ourLog.info("About to execute");
+		try {
+			iDeleteTyped.execute();
+		} catch (NullPointerException e) {
+			ourLog.error("NPE", e);
+			fail(e.toString());
+		}
 
 		assertEquals(1, myPatientResourceProvider.getCountDelete());
 
diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml
index e2edfd2bf41..d377521707b 100644
--- a/hapi-fhir-testpage-overlay/pom.xml
+++ b/hapi-fhir-testpage-overlay/pom.xml
@@ -4,7 +4,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-fhir
-		3.6.0
+		3.7.0-SNAPSHOT
 		../pom.xml
 	
 
@@ -67,7 +67,7 @@
 		
 		
 			org.thymeleaf
-			thymeleaf-spring4
+			thymeleaf-spring5
 		
 		
 			javax.servlet
diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java
index ad219e19e10..979c83148ad 100644
--- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java
+++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java
@@ -31,6 +31,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
 import org.hl7.fhir.instance.model.api.IDomainResource;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.ui.ModelMap;
+import org.thymeleaf.ITemplateEngine;
 import org.thymeleaf.TemplateEngine;
 
 import javax.servlet.ServletException;
@@ -51,17 +52,13 @@ public class BaseController {
 	private Map myContexts = new HashMap();
 	private List myFilterHeaders;
 	@Autowired
-	private TemplateEngine myTemplateEngine;
+	private ITemplateEngine myTemplateEngine;
 
 	public BaseController() {
 		super();
 	}
 
 	protected IBaseResource addCommonParams(HttpServletRequest theServletRequest, final HomeRequest theRequest, final ModelMap theModel) {
-		if (myConfig.getDebugTemplatesMode()) {
-			myTemplateEngine.getCacheManager().clearAllCaches();
-		}
-
 		final String serverId = theRequest.getServerIdWithDefault(myConfig);
 		final String serverBase = theRequest.getServerBase(theServletRequest, myConfig);
 		final String serverName = theRequest.getServerName(myConfig);
diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/FhirTesterMvcConfig.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/FhirTesterMvcConfig.java
index 73af537bc33..f5110a207f3 100644
--- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/FhirTesterMvcConfig.java
+++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/FhirTesterMvcConfig.java
@@ -1,18 +1,17 @@
 package ca.uhn.fhir.to;
 
+import ca.uhn.fhir.to.mvc.AnnotationMethodHandlerAdapterConfigurer;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
-import org.thymeleaf.spring4.SpringTemplateEngine;
-import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
-import org.thymeleaf.spring4.view.ThymeleafViewResolver;
+import org.thymeleaf.spring5.SpringTemplateEngine;
+import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
+import org.thymeleaf.spring5.view.ThymeleafViewResolver;
 import org.thymeleaf.templatemode.TemplateMode;
 
-import ca.uhn.fhir.to.mvc.AnnotationMethodHandlerAdapterConfigurer;
-
 @Configuration
 @EnableWebMvc
 @ComponentScan(basePackages = "ca.uhn.fhir.to")
diff --git a/hapi-fhir-utilities/pom.xml b/hapi-fhir-utilities/pom.xml
index 07251590edf..690c0a7621f 100644
--- a/hapi-fhir-utilities/pom.xml
+++ b/hapi-fhir-utilities/pom.xml
@@ -5,7 +5,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-deployable-pom
-		3.6.0
+		3.7.0-SNAPSHOT
 		../hapi-deployable-pom/pom.xml
 	
 
diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml
index ca44cf2c281..e35fed6fead 100644
--- a/hapi-fhir-validation-resources-dstu2.1/pom.xml
+++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml
@@ -4,7 +4,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-deployable-pom
-		3.6.0
+		3.7.0-SNAPSHOT
 		../hapi-deployable-pom/pom.xml
 	
 
diff --git a/hapi-fhir-validation-resources-dstu2/pom.xml b/hapi-fhir-validation-resources-dstu2/pom.xml
index 183bfa6e517..c5e7c04a273 100644
--- a/hapi-fhir-validation-resources-dstu2/pom.xml
+++ b/hapi-fhir-validation-resources-dstu2/pom.xml
@@ -4,7 +4,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-deployable-pom
-		3.6.0
+		3.7.0-SNAPSHOT
 		../hapi-deployable-pom/pom.xml
 	
 
diff --git a/hapi-fhir-validation-resources-dstu3/pom.xml b/hapi-fhir-validation-resources-dstu3/pom.xml
index 1a6c3f60c25..24380ff8adf 100644
--- a/hapi-fhir-validation-resources-dstu3/pom.xml
+++ b/hapi-fhir-validation-resources-dstu3/pom.xml
@@ -4,7 +4,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-deployable-pom
-		3.6.0
+		3.7.0-SNAPSHOT
 		../hapi-deployable-pom/pom.xml
 	
 
diff --git a/hapi-fhir-validation-resources-r4/pom.xml b/hapi-fhir-validation-resources-r4/pom.xml
index 2249a2450b0..40161589e9d 100644
--- a/hapi-fhir-validation-resources-r4/pom.xml
+++ b/hapi-fhir-validation-resources-r4/pom.xml
@@ -4,7 +4,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-deployable-pom
-		3.6.0
+		3.7.0-SNAPSHOT
 		../hapi-deployable-pom/pom.xml
 	
 
diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml
index 3706695e243..6723bc84d70 100644
--- a/hapi-fhir-validation/pom.xml
+++ b/hapi-fhir-validation/pom.xml
@@ -5,7 +5,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-deployable-pom
-		3.6.0
+		3.7.0-SNAPSHOT
 		../hapi-deployable-pom/pom.xml
 	
 
diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml
index 2b235261849..a96607987df 100644
--- a/hapi-tinder-plugin/pom.xml
+++ b/hapi-tinder-plugin/pom.xml
@@ -5,7 +5,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-fhir
-		3.6.0
+		3.7.0-SNAPSHOT
 		../pom.xml
 	
 
@@ -73,7 +73,7 @@
 		
 			ca.uhn.hapi.fhir
 			hapi-fhir-structures-r4
-			3.6.0
+			3.7.0-SNAPSHOT
 		
 		
 			ca.uhn.hapi.fhir
diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml
index 15d9b58ec2e..e676c751282 100644
--- a/hapi-tinder-test/pom.xml
+++ b/hapi-tinder-test/pom.xml
@@ -4,7 +4,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-fhir
-		3.6.0
+		3.7.0-SNAPSHOT
 		../pom.xml
 	
 
diff --git a/osgi/hapi-fhir-karaf-features/pom.xml b/osgi/hapi-fhir-karaf-features/pom.xml
index a3773b96d87..79738f6f4a8 100644
--- a/osgi/hapi-fhir-karaf-features/pom.xml
+++ b/osgi/hapi-fhir-karaf-features/pom.xml
@@ -6,7 +6,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-deployable-pom
-		3.6.0
+		3.7.0-SNAPSHOT
 		../../hapi-deployable-pom/pom.xml
 	
 
diff --git a/osgi/hapi-fhir-karaf-integration-tests/pom.xml b/osgi/hapi-fhir-karaf-integration-tests/pom.xml
index 0e267d54293..3e63459153c 100644
--- a/osgi/hapi-fhir-karaf-integration-tests/pom.xml
+++ b/osgi/hapi-fhir-karaf-integration-tests/pom.xml
@@ -20,7 +20,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-deployable-pom
-		3.6.0
+		3.7.0-SNAPSHOT
 		../../hapi-deployable-pom/pom.xml
 	
 
diff --git a/pom.xml b/pom.xml
index 423f900dfb4..8dc72f5173a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
 	ca.uhn.hapi.fhir
 	hapi-fhir
 	pom
-	3.6.0
+	3.7.0-SNAPSHOT
 	HAPI-FHIR
 	An open-source implementation of the FHIR specification in Java.
 	https://hapifhir.io
@@ -478,6 +478,10 @@
 			volsch
 			Volker Schmidt
 		
+		
+			magnuswatn
+			Magnus Watn
+		
 	
 
 	
@@ -518,9 +522,9 @@
 		3.0.2
 		
 		5.3.6.Final
+		5.10.3.Final
 		5.4.1.Final
 		
-		5.10.3.Final
 		4.4.6
 		4.5.3
 		2.9.7
@@ -1246,7 +1250,7 @@
 			
 			
 				org.thymeleaf
-				thymeleaf-spring4
+				thymeleaf-spring5
 				${thymeleaf-version}
 			
 			
diff --git a/restful-server-example-test/pom.xml b/restful-server-example-test/pom.xml
index d7756bed98f..ccb696aa498 100644
--- a/restful-server-example-test/pom.xml
+++ b/restful-server-example-test/pom.xml
@@ -4,7 +4,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-fhir
-		3.6.0
+		3.7.0-SNAPSHOT
 		../pom.xml
 	
 
diff --git a/restful-server-example/pom.xml b/restful-server-example/pom.xml
index 090c48e97a8..c381b959435 100644
--- a/restful-server-example/pom.xml
+++ b/restful-server-example/pom.xml
@@ -8,7 +8,7 @@
 	
 		ca.uhn.hapi.fhir
 		hapi-fhir
-		3.6.0
+		3.7.0-SNAPSHOT
 		../pom.xml
 	
 	
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index e4199967673..a9df3c26181 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -6,6 +6,103 @@
 		HAPI FHIR Changelog
 	
 	
+		
+			
+				The version of a few dependencies have been bumped to the
+				latest versions (dependent HAPI modules listed in brackets):
+				
+						
  • thymeleaf-spring4 (Testpage Overlay) has been replaced with thymeleaf-spring5
  • + + ]]> +
    + + Changed subscription processing, if the subscription criteria are straightforward (i.e. no + chained references, qualifiers or prefixes) then attempt to match the incoming resource against + the criteria in-memory. If the subscription criteria can't be matched in-memory, then the + server falls back to the original subscription matching process of querying the database. The + in-memory matcher can be disabled by setting isEnableInMemorySubscriptionMatching to "false" in + DaoConfig (by default it is true). If isEnableInMemorySubscriptionMatching is "false", then all + subscription matching will query the database as before. + + + Changed behaviour of FHIR Server to reject subscriptions with invalid criteria. If a Subscription + is submitted with invalid criteria, the server returns HTTP 422 "Unprocessable Entity" and the + Subscription is not persisted. + + + The JPA server $expunge operation could sometimes fail to expunge if + another resource linked to a resource that was being + expunged. This has been corrected. In addition, the $expunge operation + has been refactored to use smaller chunks of work + within a single DB transaction. This improves performance and reduces contention when + performing large expunge workloads. + + + A badly formatted log message when handing exceptions was cleaned up. Thanks to + Magnus Watn for the pull request! + + + A NullPointerException has been fixed when using custom resource classes that + have a @Block class as a child element. Thanks to Lars Gram Mathiasen for + reporting and providing a test case! + + + AuthorizationInterceptor now allows the GraphQL operation to be + authorized. Note that this is an all-or-nothing grant for now, it + is not yet possible to specify individual resource security when + using GraphQL. + + + The ResponseHighlighterInterceptor now declines to handle Binary responses + provided as a response from extended operations. In other words if the + operation $foo returns a Binary resource, the ResponseHighliterInterceptor will + not provide syntax highlighting on the response. This was previously the case for + the /Binary endpoint, but not for other binary responses. + + + FHIR Parser now has an additional overload of the + parseResource]]> method that accepts + an InputStream instead of a Reader as the source. + + + FHIR Fluent/Generic Client now has a new return option called + returnMethodOutcome]]> which can be + used to return a raw response. This is handy for invoking operations + that might return arbitrary binary content. + + + Moved state and functionality out of BaseHapiFhirDao.java into new classes: LogicalReferenceHelper, + ResourceIndexedSearchParams, IdHelperService, SearcchParamExtractorService, and MatchUrlService. + + + Replaced explicit @Bean construction in BaseConfig.java with @ComponentScan. Beans with state are annotated + with + @Component and stateless beans are annotated as @Service. Also changed SearchBuilder.java and the + three Subscriber classes into @Scope("protoype") so their dependencies can be @Autowired injected + as opposed to constructor parameters. + + + A bug in the JPA resource reindexer was fixed: In many cases the reindexer would + mark reindexing jobs as deleted before they had actually completed, leading to + some resources not actually being reindexed. + + + The JPA stale search deletion service now deletes cached search results in much + larger batches (20000 instead of 500) in order to reduce the amount of noise + in the logs. + + + AuthorizationInterceptor now allows arbitrary FHIR $operations to be authorized, + including support for either allowing the operation response to proceed unchallenged, + or authorizing the contents of the response. + + + An invalid SQL syntax issue has been fixed when running the CLI JPA Migrator tool against + Oracle or SQL Server. In addition, when using the "Dry Run" option, all generated SQL + statements will be logged at the end of the run. + +
    The version of a few dependencies have been bumped to the @@ -36,7 +133,7 @@ The module which deletes stale searches has been modified so that it deletes very large searches (searches with 10000+ results in the query cache) in smaller batches, in order - to avoid having very long running delete operations occupying database connections for a + to avoid having very long running delete operations occupying database connections for a long time or timing out. @@ -48,9 +145,9 @@ A new operation has been added to the JPA server called $trigger-subscription]]>. This can be used to cause a transaction to redeliver a resource that previously triggered. - See + See this link]]> - for a description of how this feature works. Note that you must add the + for a description of how this feature works. Note that you must add the SubscriptionRetriggeringProvider as shown in the sample project here.]]> diff --git a/tests/hapi-fhir-base-test-mindeps-client/pom.xml b/tests/hapi-fhir-base-test-mindeps-client/pom.xml index f718841e7d4..6d59e3167b7 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-server/pom.xml b/tests/hapi-fhir-base-test-mindeps-server/pom.xml index c1d3d361d16..d67f9a77a3b 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0 + 3.7.0-SNAPSHOT ../../pom.xml