diff --git a/.travis.yml b/.travis.yml index 52908fa3d78..333caa4ff7f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ jdk: - oraclejdk9 env: global: - - MAVEN_OPTS="-Xmx1024m" + - MAVEN_OPTS="-Xmx10244M -Xss128M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=1024M -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC" cache: directories: 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 a4fd75c4e79..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-SNAPSHOT + 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 7c78473fadb..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-SNAPSHOT + 3.7.0-SNAPSHOT ../../pom.xml diff --git a/example-projects/hapi-fhir-jpaserver-cds-example/src/test/java/ca/uhn/fhir/jpa/cds/example/CdsExampleTests.java b/example-projects/hapi-fhir-jpaserver-cds-example/src/test/java/ca/uhn/fhir/jpa/cds/example/CdsExampleTests.java index 2fc033c6b76..478accecb5b 100644 --- a/example-projects/hapi-fhir-jpaserver-cds-example/src/test/java/ca/uhn/fhir/jpa/cds/example/CdsExampleTests.java +++ b/example-projects/hapi-fhir-jpaserver-cds-example/src/test/java/ca/uhn/fhir/jpa/cds/example/CdsExampleTests.java @@ -26,6 +26,8 @@ import java.util.Collection; import java.util.List; import java.util.Scanner; +// FIXME KHS +@Ignore public class CdsExampleTests { private static IGenericClient ourClient; private static FhirContext ourCtx = FhirContext.forDstu3(); diff --git a/example-projects/hapi-fhir-jpaserver-dynamic/pom.xml b/example-projects/hapi-fhir-jpaserver-dynamic/pom.xml index 94937eae01b..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-SNAPSHOT + 3.7.0-SNAPSHOT ../../pom.xml diff --git a/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java b/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java index 35dcc65acec..67e43eb2910 100644 --- a/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java +++ b/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java @@ -54,7 +54,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { /** * Configure FHIR properties around the the JPA server via this bean */ - @Bean() + @Bean public DaoConfig daoConfig() { return FhirServerConfigCommon.getDaoConfig(); } @@ -71,7 +71,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { return FhirServerConfigCommon.getEntityManagerFactory(env, dataSource(), fhirContextDstu3()); } @@ -99,7 +99,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { return interceptor; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { return FhirServerConfigCommon.getTransactionManager(entityManagerFactory); } diff --git a/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java b/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java index c70b393e839..b35fdf54f99 100644 --- a/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java +++ b/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java @@ -57,7 +57,7 @@ public class FhirServerConfigDstu2 extends BaseJavaConfigDstu2 { * Configure FHIR properties around the the JPA server via this bean */ @SuppressWarnings("deprecation") - @Bean() + @Bean public DaoConfig daoConfig() { return FhirServerConfigCommon.getDaoConfig(); } @@ -74,7 +74,7 @@ public class FhirServerConfigDstu2 extends BaseJavaConfigDstu2 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { return FhirServerConfigCommon.getEntityManagerFactory(env, dataSource(), fhirContextDstu2()); } @@ -103,7 +103,7 @@ public class FhirServerConfigDstu2 extends BaseJavaConfigDstu2 { return interceptor; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { return FhirServerConfigCommon.getTransactionManager(entityManagerFactory); } diff --git a/example-projects/hapi-fhir-jpaserver-example-postgres/pom.xml b/example-projects/hapi-fhir-jpaserver-example-postgres/pom.xml index 56bee813797..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-SNAPSHOT + 3.7.0-SNAPSHOT ../../pom.xml diff --git a/example-projects/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java b/example-projects/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java index c2f14354b5c..c5150eac986 100644 --- a/example-projects/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java +++ b/example-projects/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java @@ -37,7 +37,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { /** * Configure FHIR properties around the the JPA server via this bean */ - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -64,13 +64,11 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean(); + LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("HAPI_PU"); retVal.setDataSource(dataSource()); - retVal.setPackagesToScan("ca.uhn.fhir.jpa.entity"); - retVal.setPersistenceProvider(new HibernatePersistenceProvider()); retVal.setJpaProperties(jpaProperties()); return retVal; } @@ -122,7 +120,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { return retVal; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); diff --git a/example-projects/hapi-fhir-standalone-overlay-example/pom.xml b/example-projects/hapi-fhir-standalone-overlay-example/pom.xml index 47279a95647..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-SNAPSHOT + 3.7.0-SNAPSHOT ../../pom.xml hapi-fhir-standalone-overlay-example diff --git a/examples/pom.xml b/examples/pom.xml index 3fa77b1ee6a..218c17630a5 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index cdb316bec57..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-SNAPSHOT + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 16718b71b5c..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-SNAPSHOT + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index a9654e28d99..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-SNAPSHOT + 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 79b26314f3f..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 */ @@ -404,8 +367,8 @@ class ModelScanner { if (paramType == null) { throw new ConfigurationException("Search param " + searchParam.name() + " has an invalid type: " + searchParam.type()); } - Set providesMembershipInCompartments = null; - providesMembershipInCompartments = new HashSet(); + Set providesMembershipInCompartments; + providesMembershipInCompartments = new HashSet<>(); for (Compartment next : searchParam.providesMembershipIn()) { if (paramType != RestSearchParameterTypeEnum.REFERENCE) { StringBuilder b = new StringBuilder(); @@ -420,14 +383,15 @@ class ModelScanner { } providesMembershipInCompartments.add(next.name()); } - + if (paramType == RestSearchParameterTypeEnum.COMPOSITE) { compositeFields.put(nextField, searchParam); continue; } - RuntimeSearchParam param = new RuntimeSearchParam(searchParam.name(), searchParam.description(), searchParam.path(), paramType, providesMembershipInCompartments, toTargetList(searchParam.target()), RuntimeSearchParamStatusEnum.ACTIVE); + Collection base = Collections.singletonList(theResourceDef.getName()); + RuntimeSearchParam param = new RuntimeSearchParam(null, null, searchParam.name(), searchParam.description(), searchParam.path(), paramType, null, providesMembershipInCompartments, toTargetList(searchParam.target()), RuntimeSearchParamStatusEnum.ACTIVE, base); theResourceDef.addSearchParam(param); nameToParam.put(param.getName(), param); } @@ -441,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); @@ -454,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/RuntimeChildResourceDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildResourceDefinition.java index b25dcbb8cdb..7db7c93400c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildResourceDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildResourceDefinition.java @@ -49,9 +49,8 @@ public class RuntimeChildResourceDefinition extends BaseRuntimeDeclaredChildDefi myResourceTypes = theResourceTypes; if (theResourceTypes == null || theResourceTypes.isEmpty()) { - myResourceTypes = new ArrayList>(); + myResourceTypes = new ArrayList<>(); myResourceTypes.add(IBaseResource.class); -// throw new ConfigurationException("Field '" + theField.getName() + "' on type '" + theField.getDeclaringClass().getCanonicalName() + "' has no resource types noted"); } } 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/fluentpath/IFluentPath.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java index b370ea014e1..8f2b8158781 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.fluentpath; */ import java.util.List; +import java.util.Optional; import org.hl7.fhir.instance.model.api.IBase; @@ -36,6 +37,15 @@ public interface IFluentPath { */ List evaluate(IBase theInput, String thePath, Class theReturnType); - + /** + * Apply the given FluentPath expression against the given input and return + * the first match (if any) + * + * @param theInput The input object (generally a resource or datatype) + * @param thePath The fluent path expression + * @param theReturnType The type to return (in order to avoid casting) + */ + Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType); + } 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/narrative/BaseThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java index b9769cb99f0..9ab0c192c8e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java @@ -34,6 +34,7 @@ import org.thymeleaf.cache.ICacheEntryValidity; import org.thymeleaf.context.Context; import org.thymeleaf.context.ITemplateContext; import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.messageresolver.IMessageResolver; import org.thymeleaf.model.IProcessableElementTag; import org.thymeleaf.processor.IProcessor; import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; @@ -65,6 +66,8 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener private HashMap myNameToNarrativeTemplate; private TemplateEngine myProfileTemplateEngine; + private IMessageResolver resolver; + /** * Constructor */ @@ -166,11 +169,21 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener }; myProfileTemplateEngine.setDialect(dialect); + if (this.resolver != null) { + myProfileTemplateEngine.setMessageResolver(this.resolver); + } } myInitialized = true; } + public void setMessageResolver(IMessageResolver resolver) { + this.resolver = resolver; + if (myProfileTemplateEngine != null && resolver != null) { + myProfileTemplateEngine.setMessageResolver(resolver); + } + } + /** * If set to true (which is the default), most whitespace will be trimmed from the generated narrative * before it is returned. 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/annotation/Operation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java index 7fdbdefa417..d17b376b08a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.annotation; * 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. 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-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/IReindexController.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RequestFormatParamStyleEnum.java similarity index 70% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/IReindexController.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RequestFormatParamStyleEnum.java index b9935057aef..4a1964d5739 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/IReindexController.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RequestFormatParamStyleEnum.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.util; +package ca.uhn.fhir.rest.api; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR - Core Library * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,15 +20,15 @@ package ca.uhn.fhir.jpa.util; * #L% */ -public interface IReindexController { +public enum RequestFormatParamStyleEnum { + /** + * Do not include a _format parameter on requests + */ + NONE, /** - * This method is called automatically by the scheduler + * "xml" or "json" */ - void performReindexingPass(); + SHORT - /** - * This method requests that the reindex process happen as soon as possible - */ - void requestReindex(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SearchTotalModeEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SearchTotalModeEnum.java index d10613d01a3..1a0d781e921 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SearchTotalModeEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/SearchTotalModeEnum.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.rest.api; +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * 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.HashMap; import java.util.Map; 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/api/IRestfulClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java index 36fd4fea5b0..45de5860581 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java @@ -1,5 +1,11 @@ package ca.uhn.fhir.rest.client.api; +import ca.uhn.fhir.context.FhirContext; +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; + import java.util.List; /* @@ -22,23 +28,15 @@ import java.util.List; * #L% */ -import org.hl7.fhir.instance.model.api.IBaseResource; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.api.SummaryEnum; - public interface IRestfulClient { /** * Retrieve the contents at the given URL and parse them as a resource. This * method could be used as a low level implementation of a read/vread/search * operation. - * - * @param theResourceType - * The resource type to parse - * @param theUrl - * The URL to load + * + * @param theResourceType The resource type to parse + * @param theUrl The URL to load * @return The parsed resource */ T fetchResourceFromUrl(Class theResourceType, String theUrl); @@ -49,6 +47,17 @@ public interface IRestfulClient { */ EncodingEnum getEncoding(); + /** + * Specifies that the client should use the given encoding to do its + * queries. This means that the client will append the "_format" param + * to GET methods (read/search/etc), and will add an appropriate header for + * write methods. + * + * @param theEncoding The encoding to use in the request, or null not specify + * an encoding (which generally implies the use of XML). The default is null. + */ + void setEncoding(EncodingEnum theEncoding); + /** * Returns the FHIR context associated with this client */ @@ -76,25 +85,12 @@ public interface IRestfulClient { */ void registerInterceptor(IClientInterceptor theInterceptor); - /** - * Specifies that the client should use the given encoding to do its - * queries. This means that the client will append the "_format" param - * to GET methods (read/search/etc), and will add an appropriate header for - * write methods. - * - * @param theEncoding - * The encoding to use in the request, or null not specify - * an encoding (which generally implies the use of XML). The default is null. - */ - void setEncoding(EncodingEnum theEncoding); - /** * Specifies that the client should request that the server respond with "pretty printing" * enabled. Note that this is a non-standard parameter, not all servers will * support it. - * - * @param thePrettyPrint - * The pretty print flag to use in the request (default is false) + * + * @param thePrettyPrint The pretty print flag to use in the request (default is false) */ void setPrettyPrint(Boolean thePrettyPrint); @@ -109,4 +105,8 @@ public interface IRestfulClient { */ void unregisterInterceptor(IClientInterceptor theInterceptor); + /** + * Configures what style of _format parameter should be used in requests + */ + void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle); } 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/IOperationUntyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java index dff17e286e1..40155444efc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java @@ -34,7 +34,7 @@ public interface IOperationUntyped { * @param theParameters The parameters to use as input. May also be null if the operation * does not require any input parameters. */ - IOperationUntypedWithInput withParameters(T theParameters); + IOperationUntypedWithInputAndPartialOutput withParameters(T theParameters); /** * The operation does not require any input parameters 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/gclient/IOperationUntypedWithInputAndPartialOutput.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java index ace3562a8c9..71594f48c47 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java @@ -38,6 +38,8 @@ public interface IOperationUntypedWithInputAndPartialOutput andParameter(String theName, IBase theValue); /** + * Adds a URL parameter to the request. + * * Use chained method calls to construct a Parameters input. This form is a convenience * in order to allow simple method chaining to be used to build up a parameters * resource for the input of an operation without needing to manually construct one. diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ReferenceClientParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ReferenceClientParam.java index 405261690b6..0b59ed2fc72 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ReferenceClientParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ReferenceClientParam.java @@ -1,9 +1,11 @@ package ca.uhn.fhir.rest.gclient; +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IIdType; + import java.util.Collection; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.primitive.IdDt; +import static org.apache.commons.lang3.StringUtils.isNotBlank; /* * #%L @@ -38,17 +40,43 @@ public class ReferenceClientParam extends BaseClientParam implements IParam { public String getParamName() { return myName; } - + + /** + * Include a chained search. For example: + *
+	 * Bundle resp = ourClient
+	 *   .search()
+	 *   .forResource(QuestionnaireResponse.class)
+	 *   .where(QuestionnaireResponse.SUBJECT.hasChainedProperty(Patient.FAMILY.matches().value("SMITH")))
+	 *   .returnBundle(Bundle.class)
+	 *   .execute();
+	 * 
+ */ public ICriterion hasChainedProperty(ICriterion theCriterion) { return new ReferenceChainCriterion(getParamName(), theCriterion); } + /** + * Include a chained search with a resource type. For example: + *
+	 * Bundle resp = ourClient
+	 *   .search()
+	 *   .forResource(QuestionnaireResponse.class)
+	 *   .where(QuestionnaireResponse.SUBJECT.hasChainedProperty("Patient", Patient.FAMILY.matches().value("SMITH")))
+	 *   .returnBundle(Bundle.class)
+	 *   .execute();
+	 * 
+ */ + public ICriterion hasChainedProperty(String theResourceType, ICriterion theCriterion) { + return new ReferenceChainCriterion(getParamName(), theResourceType, theCriterion); + } + /** * Match the referenced resource if the resource has the given ID (this can be * the logical ID or the absolute URL of the resource) */ - public ICriterion hasId(IdDt theId) { - return new StringCriterion(getParamName(), theId.getValue()); + public ICriterion hasId(IIdType theId) { + return new StringCriterion<>(getParamName(), theId.getValue()); } /** @@ -56,7 +84,7 @@ public class ReferenceClientParam extends BaseClientParam implements IParam { * the logical ID or the absolute URL of the resource) */ public ICriterion hasId(String theId) { - return new StringCriterion(getParamName(), theId); + return new StringCriterion<>(getParamName(), theId); } /** @@ -67,22 +95,28 @@ public class ReferenceClientParam extends BaseClientParam implements IParam { * with the same parameter. */ public ICriterion hasAnyOfIds(Collection theIds) { - return new StringCriterion(getParamName(), theIds); + return new StringCriterion<>(getParamName(), theIds); } private static class ReferenceChainCriterion implements ICriterion, ICriterionInternal { + private final String myResourceTypeQualifier; private String myParamName; private ICriterionInternal myWrappedCriterion; - public ReferenceChainCriterion(String theParamName, ICriterion theWrappedCriterion) { + ReferenceChainCriterion(String theParamName, ICriterion theWrappedCriterion) { + this(theParamName, null, theWrappedCriterion); + } + + ReferenceChainCriterion(String theParamName, String theResourceType, ICriterion theWrappedCriterion) { myParamName = theParamName; + myResourceTypeQualifier = isNotBlank(theResourceType) ? ":" + theResourceType : ""; myWrappedCriterion = (ICriterionInternal) theWrappedCriterion; } @Override public String getParameterName() { - return myParamName + "." + myWrappedCriterion.getParameterName(); + return myParamName + myResourceTypeQualifier + "." + myWrappedCriterion.getParameterName(); } @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java index c86d241a79f..b311d428563 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateRangeParam.java @@ -255,7 +255,7 @@ public class DateRangeParam implements IQueryParameterAnd { } public Date getLowerBoundAsInstant() { - if (myLowerBound == null) { + if (myLowerBound == null || myLowerBound.getValue() == null) { return null; } Date retVal = myLowerBound.getValue(); @@ -310,7 +310,7 @@ public class DateRangeParam implements IQueryParameterAnd { } public Date getUpperBoundAsInstant() { - if (myUpperBound == null) { + if (myUpperBound == null || myUpperBound.getValue() == null) { return null; } 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/FhirTerser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java index 354d52d6201..ec8f590d723 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java @@ -28,9 +28,9 @@ import static org.apache.commons.lang3.StringUtils.*; * 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. @@ -96,34 +96,36 @@ public class FhirTerser { /** * Clones all values from a source object into the equivalent fields in a target object - * @param theSource The source object (must not be null) - * @param theTarget The target object to copy values into (must not be null) + * + * @param theSource The source object (must not be null) + * @param theTarget The target object to copy values into (must not be null) * @param theIgnoreMissingFields The ignore fields in the target which do not exist (if false, an exception will be thrown if the target is unable to accept a value from the source) + * @return Returns the target (which will be the same object that was passed into theTarget) for easy chaining */ - public void cloneInto(IBase theSource, IBase theTarget, boolean theIgnoreMissingFields) { + public IBase cloneInto(IBase theSource, IBase theTarget, boolean theIgnoreMissingFields) { Validate.notNull(theSource, "theSource must not be null"); Validate.notNull(theTarget, "theTarget must not be null"); - + if (theSource instanceof IPrimitiveType) { if (theTarget instanceof IPrimitiveType) { - ((IPrimitiveType)theTarget).setValueAsString(((IPrimitiveType)theSource).getValueAsString()); - return; + ((IPrimitiveType) theTarget).setValueAsString(((IPrimitiveType) theSource).getValueAsString()); + return theSource; } if (theIgnoreMissingFields) { - return; + return theSource; } throw new DataFormatException("Can not copy value from primitive of type " + theSource.getClass().getName() + " into type " + theTarget.getClass().getName()); } - - BaseRuntimeElementCompositeDefinition sourceDef = (BaseRuntimeElementCompositeDefinition) myContext.getElementDefinition(theSource.getClass()); + + BaseRuntimeElementCompositeDefinition sourceDef = (BaseRuntimeElementCompositeDefinition) myContext.getElementDefinition(theSource.getClass()); BaseRuntimeElementCompositeDefinition targetDef = (BaseRuntimeElementCompositeDefinition) myContext.getElementDefinition(theTarget.getClass()); - + List children = sourceDef.getChildren(); if (sourceDef instanceof RuntimeExtensionDtDefinition) { - children = ((RuntimeExtensionDtDefinition)sourceDef).getChildrenIncludingUrl(); + children = ((RuntimeExtensionDtDefinition) sourceDef).getChildrenIncludingUrl(); } - - for (BaseRuntimeChildDefinition nextChild : children) { + + for (BaseRuntimeChildDefinition nextChild : children) for (IBase nextValue : nextChild.getAccessor().getValues(theSource)) { String elementName = nextChild.getChildNameByDatatype(nextValue.getClass()); BaseRuntimeChildDefinition targetChild = targetDef.getChildByName(elementName); @@ -133,14 +135,15 @@ public class FhirTerser { } throw new DataFormatException("Type " + theTarget.getClass().getName() + " does not have a child with name " + elementName); } - - BaseRuntimeElementDefinition childDef = targetChild.getChildByName(elementName); - IBase target = childDef.newInstance(); + + BaseRuntimeElementDefinition element = myContext.getElementDefinition(nextValue.getClass()); + IBase target = element.newInstance(); + targetChild.getMutator().addValue(theTarget, target); cloneInto(nextValue, target, theIgnoreMissingFields); } - } - + + return theTarget; } /** @@ -153,11 +156,9 @@ public class FhirTerser { * Note on scope: This method will descend into any contained resources ({@link IResource#getContained()}) as well, but will not descend into linked resources (e.g. * {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource) *

- * - * @param theResource - * The resource instance to search. Must not be null. - * @param theType - * The type to search for. Must not be null. + * + * @param theResource The resource instance to search. Must not be null. + * @param theType The type to search for. Must not be null. * @return Returns a list of all matching elements */ public List getAllPopulatedChildElementsOfType(IBaseResource theResource, final Class theType) { @@ -274,7 +275,7 @@ public class FhirTerser { .collect(Collectors.toList()); if (theAddExtension - && (!(theCurrentObj instanceof IBaseExtension) || (extensionDts.isEmpty() && theSubList.size() == 1))) { + && (!(theCurrentObj instanceof IBaseExtension) || (extensionDts.isEmpty() && theSubList.size() == 1))) { extensionDts.add(createEmptyExtensionDt((ISupportsUndeclaredExtensions) theCurrentObj, extensionUrl)); } @@ -286,7 +287,7 @@ public class FhirTerser { extensionDts = ((IBaseExtension) theCurrentObj).getExtension(); if (theAddExtension - && (extensionDts.isEmpty() && theSubList.size() == 1)) { + && (extensionDts.isEmpty() && theSubList.size() == 1)) { extensionDts.add(createEmptyExtensionDt((IBaseExtension) theCurrentObj, extensionUrl)); } @@ -311,7 +312,7 @@ public class FhirTerser { .collect(Collectors.toList()); if (theAddExtension - && (!(theCurrentObj instanceof IBaseExtension) || (extensions.isEmpty() && theSubList.size() == 1))) { + && (!(theCurrentObj instanceof IBaseExtension) || (extensions.isEmpty() && theSubList.size() == 1))) { extensions.add(createEmptyExtension((IBaseHasExtensions) theCurrentObj, extensionUrl)); } @@ -396,7 +397,7 @@ public class FhirTerser { .collect(Collectors.toList()); if (theAddExtension - && (!(theCurrentObj instanceof IBaseExtension) || (extensions.isEmpty() && theSubList.size() == 1))) { + && (!(theCurrentObj instanceof IBaseExtension) || (extensions.isEmpty() && theSubList.size() == 1))) { extensions.add(createEmptyModifierExtension((IBaseHasModifierExtensions) theCurrentObj, extensionUrl)); } @@ -478,7 +479,7 @@ public class FhirTerser { * type {@link Object}. * * @param theResource The resource instance to be accessed. Must not be null. - * @param thePath The path for the element to be accessed. + * @param thePath The path for the element to be accessed. * @return A list of values of type {@link Object}. */ public List getValues(IBaseResource theResource, String thePath) { @@ -492,8 +493,8 @@ public class FhirTerser { * type {@link Object}. * * @param theResource The resource instance to be accessed. Must not be null. - * @param thePath The path for the element to be accessed. - * @param theCreate When set to true, the terser will create a null-valued element where none exists. + * @param thePath The path for the element to be accessed. + * @param theCreate When set to true, the terser will create a null-valued element where none exists. * @return A list of values of type {@link Object}. */ public List getValues(IBaseResource theResource, String thePath, boolean theCreate) { @@ -506,9 +507,9 @@ public class FhirTerser { * Returns values stored in an element identified by its path. The list of values is of * type {@link Object}. * - * @param theResource The resource instance to be accessed. Must not be null. - * @param thePath The path for the element to be accessed. - * @param theCreate When set to true, the terser will create a null-valued element where none exists. + * @param theResource The resource instance to be accessed. Must not be null. + * @param thePath The path for the element to be accessed. + * @param theCreate When set to true, the terser will create a null-valued element where none exists. * @param theAddExtension When set to true, the terser will add a null-valued extension where one or more such extensions already exist. * @return A list of values of type {@link Object}. */ @@ -522,10 +523,10 @@ public class FhirTerser { * Returns values stored in an element identified by its path. The list of values is of * type theWantedClass. * - * @param theResource The resource instance to be accessed. Must not be null. - * @param thePath The path for the element to be accessed. + * @param theResource The resource instance to be accessed. Must not be null. + * @param thePath The path for the element to be accessed. * @param theWantedClass The desired class to be returned in a list. - * @param Type declared by theWantedClass + * @param Type declared by theWantedClass * @return A list of values of type theWantedClass. */ public List getValues(IBaseResource theResource, String thePath, Class theWantedClass) { @@ -538,11 +539,11 @@ public class FhirTerser { * Returns values stored in an element identified by its path. The list of values is of * type theWantedClass. * - * @param theResource The resource instance to be accessed. Must not be null. - * @param thePath The path for the element to be accessed. + * @param theResource The resource instance to be accessed. Must not be null. + * @param thePath The path for the element to be accessed. * @param theWantedClass The desired class to be returned in a list. - * @param theCreate When set to true, the terser will create a null-valued element where none exists. - * @param Type declared by theWantedClass + * @param theCreate When set to true, the terser will create a null-valued element where none exists. + * @param Type declared by theWantedClass * @return A list of values of type theWantedClass. */ public List getValues(IBaseResource theResource, String thePath, Class theWantedClass, boolean theCreate) { @@ -555,12 +556,12 @@ public class FhirTerser { * Returns values stored in an element identified by its path. The list of values is of * type theWantedClass. * - * @param theResource The resource instance to be accessed. Must not be null. - * @param thePath The path for the element to be accessed. - * @param theWantedClass The desired class to be returned in a list. - * @param theCreate When set to true, the terser will create a null-valued element where none exists. + * @param theResource The resource instance to be accessed. Must not be null. + * @param thePath The path for the element to be accessed. + * @param theWantedClass The desired class to be returned in a list. + * @param theCreate When set to true, the terser will create a null-valued element where none exists. * @param theAddExtension When set to true, the terser will add a null-valued extension where one or more such extensions already exist. - * @param Type declared by theWantedClass + * @param Type declared by theWantedClass * @return A list of values of type theWantedClass. */ public List getValues(IBaseResource theResource, String thePath, Class theWantedClass, boolean theCreate, boolean theAddExtension) { @@ -605,10 +606,10 @@ public class FhirTerser { /** * Returns true if theSource is in the compartment named theCompartmentName * belonging to resource theTarget - * + * * @param theCompartmentName The name of the compartment - * @param theSource The potential member of the compartment - * @param theTarget The owner of the compartment. Note that both the resource type and ID must be filled in on this IIdType or the method will throw an {@link IllegalArgumentException} + * @param theSource The potential member of the compartment + * @param theTarget The owner of the compartment. Note that both the resource type and ID must be filled in on this IIdType or the method will throw an {@link IllegalArgumentException} * @return true if theSource is in the compartment * @throws IllegalArgumentException If theTarget does not contain both a resource type and ID */ @@ -618,16 +619,16 @@ public class FhirTerser { Validate.notNull(theTarget, "theTarget must not be null"); Validate.notBlank(defaultString(theTarget.getResourceType()), "theTarget must have a populated resource type (theTarget.getResourceType() does not return a value)"); Validate.notBlank(defaultString(theTarget.getIdPart()), "theTarget must have a populated ID (theTarget.getIdPart() does not return a value)"); - + String wantRef = theTarget.toUnqualifiedVersionless().getValue(); - + RuntimeResourceDefinition sourceDef = myContext.getResourceDefinition(theSource); if (theSource.getIdElement().hasIdPart()) { if (wantRef.equals(sourceDef.getName() + '/' + theSource.getIdElement().getIdPart())) { return true; } } - + List params = sourceDef.getSearchParamsForCompartmentName(theCompartmentName); for (RuntimeSearchParam nextParam : params) { for (String nextPath : nextParam.getPathsSplit()) { @@ -679,12 +680,12 @@ public class FhirTerser { } } } - + return false; } private void visit(IBase theElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition theDefinition, IModelVisitor2 theCallback, List theContainingElementPath, - List theChildDefinitionPath, List> theElementDefinitionPath) { + List theChildDefinitionPath, List> theElementDefinitionPath) { if (theChildDefinition != null) { theChildDefinitionPath.add(theChildDefinition); } @@ -692,7 +693,7 @@ public class FhirTerser { theElementDefinitionPath.add(theDefinition); theCallback.acceptElement(theElement, Collections.unmodifiableList(theContainingElementPath), Collections.unmodifiableList(theChildDefinitionPath), - Collections.unmodifiableList(theElementDefinitionPath)); + Collections.unmodifiableList(theElementDefinitionPath)); /* * Visit undeclared extensions @@ -710,85 +711,85 @@ public class FhirTerser { * Now visit the children of the given element */ switch (theDefinition.getChildType()) { - case ID_DATATYPE: - case PRIMITIVE_XHTML_HL7ORG: - case PRIMITIVE_XHTML: - case PRIMITIVE_DATATYPE: - // These are primitive types, so we don't need to visit their children - break; - case RESOURCE: - case RESOURCE_BLOCK: - case COMPOSITE_DATATYPE: { - BaseRuntimeElementCompositeDefinition childDef = (BaseRuntimeElementCompositeDefinition) theDefinition; - for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) { - List values = nextChild.getAccessor().getValues(theElement); - if (values != null) { - for (IBase nextValue : values) { - if (nextValue == null) { - continue; - } - if (nextValue.isEmpty()) { - continue; - } - BaseRuntimeElementDefinition childElementDef; - childElementDef = nextChild.getChildElementDefinitionByDatatype(nextValue.getClass()); - - if (childElementDef == null) { - StringBuilder b = new StringBuilder(); - b.append("Found value of type["); - b.append(nextValue.getClass().getSimpleName()); - b.append("] which is not valid for field["); - b.append(nextChild.getElementName()); - b.append("] in "); - b.append(childDef.getName()); - b.append(" - Valid types: "); - for (Iterator iter = new TreeSet(nextChild.getValidChildNames()).iterator(); iter.hasNext();) { - BaseRuntimeElementDefinition childByName = nextChild.getChildByName(iter.next()); - b.append(childByName.getImplementingClass().getSimpleName()); - if (iter.hasNext()) { - b.append(", "); - } + case ID_DATATYPE: + case PRIMITIVE_XHTML_HL7ORG: + case PRIMITIVE_XHTML: + case PRIMITIVE_DATATYPE: + // These are primitive types, so we don't need to visit their children + break; + case RESOURCE: + case RESOURCE_BLOCK: + case COMPOSITE_DATATYPE: { + BaseRuntimeElementCompositeDefinition childDef = (BaseRuntimeElementCompositeDefinition) theDefinition; + for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) { + List values = nextChild.getAccessor().getValues(theElement); + if (values != null) { + for (IBase nextValue : values) { + if (nextValue == null) { + continue; } - throw new DataFormatException(b.toString()); - } + if (nextValue.isEmpty()) { + continue; + } + BaseRuntimeElementDefinition childElementDef; + childElementDef = nextChild.getChildElementDefinitionByDatatype(nextValue.getClass()); - if (nextChild instanceof RuntimeChildDirectResource) { - // Don't descend into embedded resources - theContainingElementPath.add(nextValue); - theChildDefinitionPath.add(nextChild); - theElementDefinitionPath.add(myContext.getElementDefinition(nextValue.getClass())); - theCallback.acceptElement(nextValue, Collections.unmodifiableList(theContainingElementPath), Collections.unmodifiableList(theChildDefinitionPath), + if (childElementDef == null) { + StringBuilder b = new StringBuilder(); + b.append("Found value of type["); + b.append(nextValue.getClass().getSimpleName()); + b.append("] which is not valid for field["); + b.append(nextChild.getElementName()); + b.append("] in "); + b.append(childDef.getName()); + b.append(" - Valid types: "); + for (Iterator iter = new TreeSet(nextChild.getValidChildNames()).iterator(); iter.hasNext(); ) { + BaseRuntimeElementDefinition childByName = nextChild.getChildByName(iter.next()); + b.append(childByName.getImplementingClass().getSimpleName()); + if (iter.hasNext()) { + b.append(", "); + } + } + throw new DataFormatException(b.toString()); + } + + if (nextChild instanceof RuntimeChildDirectResource) { + // Don't descend into embedded resources + theContainingElementPath.add(nextValue); + theChildDefinitionPath.add(nextChild); + theElementDefinitionPath.add(myContext.getElementDefinition(nextValue.getClass())); + theCallback.acceptElement(nextValue, Collections.unmodifiableList(theContainingElementPath), Collections.unmodifiableList(theChildDefinitionPath), Collections.unmodifiableList(theElementDefinitionPath)); - theChildDefinitionPath.remove(theChildDefinitionPath.size() - 1); - theContainingElementPath.remove(theContainingElementPath.size() - 1); - theElementDefinitionPath.remove(theElementDefinitionPath.size() - 1); - } else { - visit(nextValue, nextChild, childElementDef, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath); + theChildDefinitionPath.remove(theChildDefinitionPath.size() - 1); + theContainingElementPath.remove(theContainingElementPath.size() - 1); + theElementDefinitionPath.remove(theElementDefinitionPath.size() - 1); + } else { + visit(nextValue, nextChild, childElementDef, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath); + } } } } + break; } - break; - } - case CONTAINED_RESOURCES: { - BaseContainedDt value = (BaseContainedDt) theElement; - for (IResource next : value.getContainedResources()) { - BaseRuntimeElementCompositeDefinition def = myContext.getResourceDefinition(next); - visit(next, null, def, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath); + case CONTAINED_RESOURCES: { + BaseContainedDt value = (BaseContainedDt) theElement; + for (IResource next : value.getContainedResources()) { + BaseRuntimeElementCompositeDefinition def = myContext.getResourceDefinition(next); + visit(next, null, def, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath); + } + break; } - break; - } - case EXTENSION_DECLARED: - case UNDECL_EXT: { - throw new IllegalStateException("state should not happen: " + theDefinition.getChildType()); - } - case CONTAINED_RESOURCE_LIST: { - if (theElement != null) { - BaseRuntimeElementDefinition def = myContext.getElementDefinition(theElement.getClass()); - visit(theElement, null, def, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath); + case EXTENSION_DECLARED: + case UNDECL_EXT: { + throw new IllegalStateException("state should not happen: " + theDefinition.getChildType()); + } + case CONTAINED_RESOURCE_LIST: { + if (theElement != null) { + BaseRuntimeElementDefinition def = myContext.getElementDefinition(theElement.getClass()); + visit(theElement, null, def, theCallback, theContainingElementPath, theChildDefinitionPath, theElementDefinitionPath); + } + break; } - break; - } } if (theChildDefinition != null) { @@ -800,16 +801,14 @@ public class FhirTerser { /** * Visit all elements in a given resource - * + * *

* Note on scope: This method will descend into any contained resources ({@link IResource#getContained()}) as well, but will not descend into linked resources (e.g. * {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource) *

- * - * @param theResource - * The resource to visit - * @param theVisitor - * The visitor + * + * @param theResource The resource to visit + * @param theVisitor The visitor */ public void visit(IBaseResource theResource, IModelVisitor theVisitor) { BaseRuntimeElementCompositeDefinition def = myContext.getResourceDefinition(theResource); @@ -818,18 +817,16 @@ public class FhirTerser { /** * Visit all elements in a given resource - * + *

* THIS ALTERNATE METHOD IS STILL EXPERIMENTAL - * + * *

* Note on scope: This method will descend into any contained resources ({@link IResource#getContained()}) as well, but will not descend into linked resources (e.g. * {@link BaseResourceReferenceDt#getResource()}) or embedded resources (e.g. Bundle.entry.resource) *

- * - * @param theResource - * The resource to visit - * @param theVisitor - * The visitor + * + * @param theResource The resource to visit + * @param theVisitor The visitor */ void visit(IBaseResource theResource, IModelVisitor2 theVisitor) { BaseRuntimeElementCompositeDefinition def = myContext.getResourceDefinition(theResource); @@ -837,22 +834,22 @@ public class FhirTerser { } private void visit(IdentityHashMap theStack, IBaseResource theResource, IBase theElement, List thePathToElement, BaseRuntimeChildDefinition theChildDefinition, - BaseRuntimeElementDefinition theDefinition, IModelVisitor theCallback) { + BaseRuntimeElementDefinition theDefinition, IModelVisitor theCallback) { List pathToElement = addNameToList(thePathToElement, theChildDefinition); if (theStack.put(theElement, theElement) != null) { return; } - + theCallback.acceptElement(theResource, theElement, pathToElement, theChildDefinition, theDefinition); BaseRuntimeElementDefinition def = theDefinition; if (def.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) { def = myContext.getElementDefinition(theElement.getClass()); } - + if (theElement instanceof IBaseReference) { - IBaseResource target = ((IBaseReference)theElement).getResource(); + IBaseResource target = ((IBaseReference) theElement).getResource(); if (target != null) { if (target.getIdElement().hasIdPart() == false || target.getIdElement().isLocal()) { RuntimeResourceDefinition targetDef = myContext.getResourceDefinition(target); @@ -860,71 +857,71 @@ public class FhirTerser { } } } - + switch (def.getChildType()) { - case ID_DATATYPE: - case PRIMITIVE_XHTML_HL7ORG: - case PRIMITIVE_XHTML: - case PRIMITIVE_DATATYPE: - // These are primitive types - break; - case RESOURCE: - case RESOURCE_BLOCK: - case COMPOSITE_DATATYPE: { - BaseRuntimeElementCompositeDefinition childDef = (BaseRuntimeElementCompositeDefinition) def; - for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) { - - List values = nextChild.getAccessor().getValues(theElement); - if (values != null) { - for (Object nextValueObject : values) { - IBase nextValue; - try { - nextValue = (IBase) nextValueObject; - } catch (ClassCastException e) { - String s = "Found instance of " + nextValueObject.getClass() + " - Did you set a field value to the incorrect type? Expected " + IBase.class.getName(); - throw new ClassCastException(s); - } - if (nextValue == null) { - continue; - } - if (nextValue.isEmpty()) { - continue; - } - BaseRuntimeElementDefinition childElementDef; - childElementDef = nextChild.getChildElementDefinitionByDatatype(nextValue.getClass()); + case ID_DATATYPE: + case PRIMITIVE_XHTML_HL7ORG: + case PRIMITIVE_XHTML: + case PRIMITIVE_DATATYPE: + // These are primitive types + break; + case RESOURCE: + case RESOURCE_BLOCK: + case COMPOSITE_DATATYPE: { + BaseRuntimeElementCompositeDefinition childDef = (BaseRuntimeElementCompositeDefinition) def; + for (BaseRuntimeChildDefinition nextChild : childDef.getChildrenAndExtension()) { - if (childElementDef == null) { - childElementDef = myContext.getElementDefinition(nextValue.getClass()); - } + List values = nextChild.getAccessor().getValues(theElement); + if (values != null) { + for (Object nextValueObject : values) { + IBase nextValue; + try { + nextValue = (IBase) nextValueObject; + } catch (ClassCastException e) { + String s = "Found instance of " + nextValueObject.getClass() + " - Did you set a field value to the incorrect type? Expected " + IBase.class.getName(); + throw new ClassCastException(s); + } + if (nextValue == null) { + continue; + } + if (nextValue.isEmpty()) { + continue; + } + BaseRuntimeElementDefinition childElementDef; + childElementDef = nextChild.getChildElementDefinitionByDatatype(nextValue.getClass()); - if (nextChild instanceof RuntimeChildDirectResource) { - // Don't descend into embedded resources - theCallback.acceptElement(theResource, nextValue, null, nextChild, childElementDef); - } else { - visit(theStack, theResource, nextValue, pathToElement, nextChild, childElementDef, theCallback); + if (childElementDef == null) { + childElementDef = myContext.getElementDefinition(nextValue.getClass()); + } + + if (nextChild instanceof RuntimeChildDirectResource) { + // Don't descend into embedded resources + theCallback.acceptElement(theResource, nextValue, null, nextChild, childElementDef); + } else { + visit(theStack, theResource, nextValue, pathToElement, nextChild, childElementDef, theCallback); + } } } } + break; } - break; - } - case CONTAINED_RESOURCES: { - BaseContainedDt value = (BaseContainedDt) theElement; - for (IResource next : value.getContainedResources()) { - def = myContext.getResourceDefinition(next); - visit(theStack, next, next, pathToElement, null, def, theCallback); + case CONTAINED_RESOURCES: { + BaseContainedDt value = (BaseContainedDt) theElement; + for (IResource next : value.getContainedResources()) { + def = myContext.getResourceDefinition(next); + visit(theStack, next, next, pathToElement, null, def, theCallback); + } + break; + } + case CONTAINED_RESOURCE_LIST: + case EXTENSION_DECLARED: + case UNDECL_EXT: { + throw new IllegalStateException("state should not happen: " + def.getChildType()); } - break; } - case CONTAINED_RESOURCE_LIST: - case EXTENSION_DECLARED: - case UNDECL_EXT: { - throw new IllegalStateException("state should not happen: " + def.getChildType()); - } - } - + theStack.remove(theElement); - + } } 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-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 6c18ab8b798..c3c435c85b1 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -61,7 +61,6 @@ ca.uhn.fhir.validation.ValidationResult.noIssuesDetected=No issues detected duri ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceVersionConstraintFailure=The operation has failed with a version constraint failure. This generally means that two clients/threads were trying to update the same resource at the same time, and this request was chosen as the failing request. ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceIndexedCompositeStringUniqueConstraintFailure=The operation has failed with a unique index constraint failure. This probably means that the operation was trying to create/update a resource that would have resulted in a duplicate value for a unique index. -ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.externalReferenceNotAllowed=Resource contains external reference to URL "{0}" but this server is not configured to allow external references ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contains resource with operation NOOP. This is only valid as a response operation, not in a request ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}" ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlNoMatches=Invalid match URL "{0}" - No resources match this search @@ -82,6 +81,7 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithInvalidId=Can not ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.incorrectResourceType=Incorrect resource type detected for endpoint, found {0} but expected {1} ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithClientAssignedNumericId=Can not create resource with ID[{0}], no resource with this ID exists and clients may only assign IDs which contain at least one non-numeric character ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithClientAssignedId=Can not create resource with ID[{0}], ID must not be supplied on a create (POST) operation +ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.failedToCreateWithClientAssignedIdNotAllowed=No resource exists on this server resource with ID[{0}], and client-assigned IDs are not enabled. ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidParameterChain=Invalid parameter chain: {0} ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidVersion=Version "{0}" is not valid for resource {1} ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.multipleParamsWithSameNameOneIsMissingTrue=This server does not know how to handle multiple "{0}" parameters where one has a value of :missing=true @@ -91,7 +91,8 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulUpdate=Successfully update ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulDeletes=Successfully deleted {0} resource(s) in {1}ms ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSearchParameter=Unknown search parameter "{0}". Value search parameters for this search are: {1} -ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor.failedToExtractPaths=Failed to extract values from resource using FHIRPath "{0}": {1} +ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.externalReferenceNotAllowed=Resource contains external reference to URL "{0}" but this server is not configured to allow external references +ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.failedToExtractPaths=Failed to extract values from resource using FHIRPath "{0}": {1} ca.uhn.fhir.jpa.dao.SearchBuilder.invalidQuantityPrefix=Unable to handle quantity prefix "{0}" for value: {1} ca.uhn.fhir.jpa.dao.SearchBuilder.invalidNumberPrefix=Unable to handle number prefix "{0}" for value: {1} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/param/DateRangeParamTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/param/DateRangeParamTest.java new file mode 100644 index 00000000000..72b03f5b4c9 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/param/DateRangeParamTest.java @@ -0,0 +1,67 @@ +package ca.uhn.fhir.rest.param; + +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.QualifiedParamList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +@RunWith(JUnit4.class) +public class DateRangeParamTest { + private FhirContext fhirContext; + + @Before + public void initMockContext() { + fhirContext = Mockito.mock(FhirContext.class); + } + + /** Can happen e.g. when the query parameter for {@code _lastUpdated} is left empty. */ + @Test + public void testParamWithoutPrefixAndWithoutValue() { + QualifiedParamList qualifiedParamList = new QualifiedParamList(1); + qualifiedParamList.add(""); + + List params = new ArrayList<>(1); + params.add(qualifiedParamList); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setValuesAsQueryTokens(fhirContext, "_lastUpdated", params); + + assertTrue(dateRangeParam.isEmpty()); + } + + /** Can happen e.g. when the query parameter for {@code _lastUpdated} is given as {@code lt} without any value. */ + @Test + public void testUpperBoundWithPrefixWithoutValue() { + QualifiedParamList qualifiedParamList = new QualifiedParamList(1); + qualifiedParamList.add("lt"); + + List params = new ArrayList<>(1); + params.add(qualifiedParamList); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setValuesAsQueryTokens(fhirContext, "_lastUpdated", params); + + assertTrue(dateRangeParam.isEmpty()); + } + + /** Can happen e.g. when the query parameter for {@code _lastUpdated} is given as {@code gt} without any value. */ + @Test + public void testLowerBoundWithPrefixWithoutValue() { + QualifiedParamList qualifiedParamList = new QualifiedParamList(1); + qualifiedParamList.add("gt"); + + List params = new ArrayList<>(1); + params.add(qualifiedParamList); + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setValuesAsQueryTokens(fhirContext, "_lastUpdated", params); + + assertTrue(dateRangeParam.isEmpty()); + } +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 35ee4b803dd..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-SNAPSHOT + 3.7.0-SNAPSHOT ../../hapi-deployable-pom/pom.xml @@ -47,7 +47,6 @@ org.apache.commons commons-compress - 1.14 ca.uhn.hapi.fhir @@ -189,7 +188,7 @@ org.thymeleaf - thymeleaf-spring4 + thymeleaf-spring5 diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java index 66fac50c071..a77041fd8d3 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java @@ -256,27 +256,30 @@ public abstract class BaseApp { System.err.println("" + ansi().fg(Ansi.Color.WHITE).boldOff()); logCommandUsageNoHeader(command); runCleanupHookAndUnregister(); - System.exit(1); + exitDueToException(e); } catch (CommandFailureException e) { ourLog.error(e.getMessage()); runCleanupHookAndUnregister(); - if ("true".equals(System.getProperty("test"))) { - throw e; - } else { - System.exit(1); - } + exitDueToException(e); } catch (Throwable t) { ourLog.error("Error during execution: ", t); runCleanupHookAndUnregister(); - if ("true".equals(System.getProperty("test"))) { - throw new CommandFailureException("Error: " + t.toString(), t); - } else { - System.exit(1); - } + exitDueToException(new CommandFailureException("Error: " + t.toString(), t)); } } + private void exitDueToException(Throwable e) { + if ("true".equals(System.getProperty("test"))) { + if (e instanceof CommandFailureException) { + throw (CommandFailureException)e; + } + throw new Error(e); + } else { + System.exit(1); + } + } + private void runCleanupHookAndUnregister() { if (myShutdownHookHasNotRun) { Runtime.getRuntime().removeShutdownHook(myShutdownHook); diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseMigrateDatabaseCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseMigrateDatabaseCommand.java index f6e9f2569ab..3b9e73761e2 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseMigrateDatabaseCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseMigrateDatabaseCommand.java @@ -25,15 +25,24 @@ import ca.uhn.fhir.jpa.migrate.Migrator; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; +import static org.apache.commons.lang3.StringUtils.defaultString; + public abstract class BaseMigrateDatabaseCommand extends BaseCommand { private static final String MIGRATE_DATABASE = "migrate-database"; + private Set myFlags; + + protected Set getFlags() { + return myFlags; + } @Override public String getCommandDescription() { @@ -68,6 +77,7 @@ public abstract class BaseMigrateDatabaseCommand extends BaseCom addRequiredOption(retVal, "f", "from", "Version", "The database schema version to migrate FROM"); addRequiredOption(retVal, "t", "to", "Version", "The database schema version to migrate TO"); addRequiredOption(retVal, "d", "driver", "Driver", "The database driver to use (Options are " + driverOptions() + ")"); + addOptionalOption(retVal, "x", "flags", "Flags", "A comma-separated list of any specific migration flags (these flags are version specific, see migrator documentation for details)"); return retVal; } @@ -97,6 +107,12 @@ public abstract class BaseMigrateDatabaseCommand extends BaseCom boolean dryRun = theCommandLine.hasOption("r"); + String flags = theCommandLine.getOptionValue("x"); + myFlags = Arrays.stream(defaultString(flags).split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + Migrator migrator = new Migrator(); migrator.setConnectionUrl(url); migrator.setDriverType(driverType); diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/HapiMigrateDatabaseCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/HapiMigrateDatabaseCommand.java index ff7d39c0fda..6be5b241110 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/HapiMigrateDatabaseCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/HapiMigrateDatabaseCommand.java @@ -42,7 +42,7 @@ public class HapiMigrateDatabaseCommand extends BaseMigrateDatabaseCommand> tasks = new HapiFhirJpaMigrationTasks().getTasks(theFrom, theTo); + List> tasks = new HapiFhirJpaMigrationTasks(getFlags()).getTasks(theFrom, theTo); tasks.forEach(theMigrator::addTask); } } diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/HapiMigrateDatabaseCommandTest.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/HapiMigrateDatabaseCommandTest.java index 7051011ff58..0f0a207744f 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/HapiMigrateDatabaseCommandTest.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/HapiMigrateDatabaseCommandTest.java @@ -7,14 +7,26 @@ import org.apache.commons.io.IOUtils; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback; +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobCreator; import java.io.File; import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class HapiMigrateDatabaseCommandTest { @@ -25,39 +37,64 @@ public class HapiMigrateDatabaseCommandTest { } @Test - public void testMigrate() throws IOException { + public void testMigrate_340_370() throws IOException { + + File directory = new File("target/migrator_derby_test_340_360"); + if (directory.exists()) { + FileUtils.deleteDirectory(directory); + } + + String url = "jdbc:derby:directory:" + directory.getAbsolutePath() + ";create=true"; + DriverTypeEnum.ConnectionProperties connectionProperties = DriverTypeEnum.DERBY_EMBEDDED.newConnectionProperties(url, "", ""); + + String initSql = "/persistence_create_derby107_340.sql"; + executeSqlStatements(connectionProperties, initSql); + + seedDatabase340(connectionProperties); + + ourLog.info("**********************************************"); + ourLog.info("Done Setup, Starting Migration..."); + ourLog.info("**********************************************"); + + String[] args = new String[]{ + "migrate-database", + "-d", "DERBY_EMBEDDED", + "-u", url, + "-n", "", + "-p", "", + "-f", "V3_4_0", + "-t", "V3_7_0" + }; + App.main(args); + + connectionProperties.getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = connectionProperties.newJdbcTemplate(); + List> values = jdbcTemplate.queryForList("SELECT * FROM hfj_spidx_token"); + assertEquals(1, values.size()); + assertEquals("identifier", values.get(0).get("SP_NAME")); + assertEquals("12345678", values.get(0).get("SP_VALUE")); + assertTrue(values.get(0).keySet().contains("HASH_IDENTITY")); + assertEquals(7001889285610424179L, values.get(0).get("HASH_IDENTITY")); + return null; + }); + } + + + @Test + public void testMigrate_340_350() throws IOException { File directory = new File("target/migrator_derby_test_340_350"); if (directory.exists()) { FileUtils.deleteDirectory(directory); } - String url = "jdbc:derby:directory:target/migrator_derby_test_340_350;create=true"; + String url = "jdbc:derby:directory:" + directory.getAbsolutePath() + ";create=true"; DriverTypeEnum.ConnectionProperties connectionProperties = DriverTypeEnum.DERBY_EMBEDDED.newConnectionProperties(url, "", ""); - String script = IOUtils.toString(HapiMigrateDatabaseCommandTest.class.getResourceAsStream("/persistence_create_derby107_340.sql"), Charsets.UTF_8); - List scriptStatements = new ArrayList<>(Arrays.asList(script.split("\n"))); - for (int i = 0; i < scriptStatements.size(); i++) { - String nextStatement = scriptStatements.get(i); - if (isBlank(nextStatement)) { - scriptStatements.remove(i); - i--; - continue; - } + String initSql = "/persistence_create_derby107_340.sql"; + executeSqlStatements(connectionProperties, initSql); - nextStatement = nextStatement.trim(); - while (nextStatement.endsWith(";")) { - nextStatement = nextStatement.substring(0, nextStatement.length() - 1); - } - scriptStatements.set(i, nextStatement); - } - - connectionProperties.getTxTemplate().execute(t -> { - for (String next : scriptStatements) { - connectionProperties.newJdbcTemplate().execute(next); - } - return null; - }); + seedDatabase340(connectionProperties); ourLog.info("**********************************************"); ourLog.info("Done Setup, Starting Dry Run..."); @@ -75,6 +112,13 @@ public class HapiMigrateDatabaseCommandTest { }; App.main(args); + connectionProperties.getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = connectionProperties.newJdbcTemplate(); + List> values = jdbcTemplate.queryForList("SELECT * FROM hfj_spidx_token"); + assertFalse(values.get(0).keySet().contains("HASH_IDENTITY")); + return null; + }); + ourLog.info("**********************************************"); ourLog.info("Done Setup, Starting Migration..."); ourLog.info("**********************************************"); @@ -89,5 +133,203 @@ public class HapiMigrateDatabaseCommandTest { "-t", "V3_5_0" }; App.main(args); + + connectionProperties.getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = connectionProperties.newJdbcTemplate(); + List> values = jdbcTemplate.queryForList("SELECT * FROM hfj_spidx_token"); + assertEquals(1, values.size()); + assertEquals("identifier", values.get(0).get("SP_NAME")); + assertEquals("12345678", values.get(0).get("SP_VALUE")); + assertTrue(values.get(0).keySet().contains("HASH_IDENTITY")); + assertEquals(7001889285610424179L, values.get(0).get("HASH_IDENTITY")); + return null; + }); } + + + private void seedDatabase340(DriverTypeEnum.ConnectionProperties theConnectionProperties) { + theConnectionProperties.getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = theConnectionProperties.newJdbcTemplate(); + + jdbcTemplate.execute( + "insert into HFJ_RESOURCE (RES_DELETED_AT, RES_VERSION, FORCED_ID_PID, HAS_TAGS, RES_PUBLISHED, RES_UPDATED, SP_HAS_LINKS, HASH_SHA256, SP_INDEX_STATUS, RES_LANGUAGE, SP_CMPSTR_UNIQ_PRESENT, SP_COORDS_PRESENT, SP_DATE_PRESENT, SP_NUMBER_PRESENT, SP_QUANTITY_PRESENT, SP_STRING_PRESENT, SP_TOKEN_PRESENT, SP_URI_PRESENT, RES_PROFILE, RES_TYPE, RES_VER, RES_ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + new AbstractLobCreatingPreparedStatementCallback(new DefaultLobHandler()) { + @Override + protected void setValues(PreparedStatement thePs, LobCreator theLobCreator) throws SQLException { + thePs.setNull(1, Types.TIMESTAMP); + thePs.setString(2, "R4"); + thePs.setNull(3, Types.BIGINT); + thePs.setBoolean(4, false); + thePs.setTimestamp(5, new Timestamp(System.currentTimeMillis())); + thePs.setTimestamp(6, new Timestamp(System.currentTimeMillis())); + thePs.setBoolean(7, false); + thePs.setNull(8, Types.VARCHAR); + thePs.setLong(9, 1L); + thePs.setNull(10, Types.VARCHAR); + thePs.setBoolean(11, false); + thePs.setBoolean(12, false); + thePs.setBoolean(13, false); + thePs.setBoolean(14, false); + thePs.setBoolean(15, false); + thePs.setBoolean(16, false); + thePs.setBoolean(17, false); + thePs.setBoolean(18, false); + thePs.setNull(19, Types.VARCHAR); + thePs.setString(20, "Patient"); + thePs.setLong(21, 1L); + thePs.setLong(22, 1L); + } + } + ); + + jdbcTemplate.execute( + "insert into HFJ_RES_VER (RES_DELETED_AT, RES_VERSION, FORCED_ID_PID, HAS_TAGS, RES_PUBLISHED, RES_UPDATED, RES_ENCODING, RES_TEXT, RES_ID, RES_TYPE, RES_VER, PID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + new AbstractLobCreatingPreparedStatementCallback(new DefaultLobHandler()) { + @Override + protected void setValues(PreparedStatement thePs, LobCreator theLobCreator) throws SQLException { + thePs.setNull(1, Types.TIMESTAMP); + thePs.setString(2, "R4"); + thePs.setNull(3, Types.BIGINT); + thePs.setBoolean(4, false); + thePs.setTimestamp(5, new Timestamp(System.currentTimeMillis())); + thePs.setTimestamp(6, new Timestamp(System.currentTimeMillis())); + thePs.setString(7, "JSON"); + theLobCreator.setBlobAsBytes(thePs, 8, "{\"resourceType\":\"Patient\"}".getBytes(Charsets.US_ASCII)); + thePs.setLong(9, 1L); + thePs.setString(10, "Patient"); + thePs.setLong(11, 1L); + thePs.setLong(12, 1L); + } + } + ); + + jdbcTemplate.execute( + "insert into HFJ_SPIDX_STRING (SP_MISSING, SP_NAME, RES_ID, RES_TYPE, SP_UPDATED, SP_VALUE_EXACT, SP_VALUE_NORMALIZED, SP_ID) values (?, ?, ?, ?, ?, ?, ?, ?)", + new AbstractLobCreatingPreparedStatementCallback(new DefaultLobHandler()) { + @Override + protected void setValues(PreparedStatement thePs, LobCreator theLobCreator) throws SQLException { + thePs.setBoolean(1, false); + thePs.setString(2, "given"); + thePs.setLong(3, 1L); // res-id + thePs.setString(4, "Patient"); + thePs.setTimestamp(5, new Timestamp(System.currentTimeMillis())); + thePs.setString(6, "ROBERT"); + thePs.setString(7, "Robert"); + thePs.setLong(8, 1L); + } + } + ); + + jdbcTemplate.execute( + "insert into HFJ_SPIDX_TOKEN (SP_MISSING, SP_NAME, RES_ID, RES_TYPE, SP_UPDATED, SP_SYSTEM, SP_VALUE, SP_ID) values (?, ?, ?, ?, ?, ?, ?, ?)", + new AbstractLobCreatingPreparedStatementCallback(new DefaultLobHandler()) { + @Override + protected void setValues(PreparedStatement thePs, LobCreator theLobCreator) throws SQLException { + thePs.setBoolean(1, false); + thePs.setString(2, "identifier"); + thePs.setLong(3, 1L); // res-id + thePs.setString(4, "Patient"); + thePs.setTimestamp(5, new Timestamp(System.currentTimeMillis())); + thePs.setString(6, "http://foo"); + thePs.setString(7, "12345678"); + thePs.setLong(8, 1L); + } + } + ); + + jdbcTemplate.execute( + "insert into HFJ_SPIDX_DATE (SP_MISSING, SP_NAME, RES_ID, RES_TYPE, SP_UPDATED, SP_VALUE_HIGH, SP_VALUE_LOW, SP_ID) values (?, ?, ?, ?, ?, ?, ?, ?)", + new AbstractLobCreatingPreparedStatementCallback(new DefaultLobHandler()) { + @Override + protected void setValues(PreparedStatement thePs, LobCreator theLobCreator) throws SQLException { + thePs.setBoolean(1, false); + thePs.setString(2, "birthdate"); + thePs.setLong(3, 1L); // res-id + thePs.setString(4, "Patient"); + thePs.setTimestamp(5, new Timestamp(System.currentTimeMillis())); + thePs.setTimestamp(6, new Timestamp(1000000000L)); // value high + thePs.setTimestamp(7, new Timestamp(1000000000L)); // value low + thePs.setLong(8, 1L); + } + } + ); + + return null; + }); + + } + + + @Test + public void testMigrate_340_350_NoMigrateHashes() throws IOException { + + File directory = new File("target/migrator_derby_test_340_350_nmh"); + if (directory.exists()) { + FileUtils.deleteDirectory(directory); + } + + String url = "jdbc:derby:directory:" + directory.getAbsolutePath() + ";create=true"; + DriverTypeEnum.ConnectionProperties connectionProperties = DriverTypeEnum.DERBY_EMBEDDED.newConnectionProperties(url, "", ""); + + String initSql = "/persistence_create_derby107_340.sql"; + executeSqlStatements(connectionProperties, initSql); + + seedDatabase340(connectionProperties); + + ourLog.info("**********************************************"); + ourLog.info("Done Setup, Starting Migration..."); + ourLog.info("**********************************************"); + + String[] args = new String[]{ + "migrate-database", + "-d", "DERBY_EMBEDDED", + "-u", url, + "-n", "", + "-p", "", + "-f", "V3_4_0", + "-t", "V3_5_0", + "-x", "no-migrate-350-hashes" + }; + App.main(args); + + connectionProperties.getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = connectionProperties.newJdbcTemplate(); + List> values = jdbcTemplate.queryForList("SELECT * FROM hfj_spidx_token"); + assertEquals(1, values.size()); + assertEquals("identifier", values.get(0).get("SP_NAME")); + assertEquals("12345678", values.get(0).get("SP_VALUE")); + assertEquals(null, values.get(0).get("HASH_IDENTITY")); + return null; + }); + + } + + private void executeSqlStatements(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theInitSql) throws + IOException { + String script = IOUtils.toString(HapiMigrateDatabaseCommandTest.class.getResourceAsStream(theInitSql), Charsets.UTF_8); + List scriptStatements = new ArrayList<>(Arrays.asList(script.split("\n"))); + for (int i = 0; i < scriptStatements.size(); i++) { + String nextStatement = scriptStatements.get(i); + if (isBlank(nextStatement)) { + scriptStatements.remove(i); + i--; + continue; + } + + nextStatement = nextStatement.trim(); + while (nextStatement.endsWith(";")) { + nextStatement = nextStatement.substring(0, nextStatement.length() - 1); + } + scriptStatements.set(i, nextStatement); + } + + theConnectionProperties.getTxTemplate().execute(t -> { + for (String next : scriptStatements) { + theConnectionProperties.newJdbcTemplate().execute(next); + } + return null; + }); + + } + } diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index 8db8f3a2b24..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-SNAPSHOT + 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 f261b8cb01c..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-SNAPSHOT + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirDbConfig.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirDbConfig.java index 19a7706e450..ddd2ffe9773 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirDbConfig.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirDbConfig.java @@ -37,7 +37,7 @@ public class FhirDbConfig { return retVal; } - @Bean() + @Bean public Properties jpaProperties() { Properties extraProperties = new Properties(); extraProperties.put("hibernate.dialect", DerbyTenSevenHapiFhirDialect.class.getName()); diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java index 32270707137..9f1d68a277a 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java @@ -36,7 +36,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 { /** * Configure FHIR properties around the the JPA server via this bean */ - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -47,13 +47,11 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("HAPI_PU"); retVal.setDataSource(myDataSource); - retVal.setPackagesToScan("ca.uhn.fhir.jpa.entity"); - retVal.setPersistenceProvider(new HibernatePersistenceProvider()); retVal.setJpaProperties(myJpaProperties); return retVal; } @@ -87,7 +85,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 { return retVal; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java index 88053206124..25f47534628 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java @@ -2,14 +2,18 @@ package ca.uhn.fhir.jpa.demo; import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import org.apache.commons.lang3.time.DateUtils; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -42,7 +46,7 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 { /** * Configure FHIR properties around the the JPA server via this bean */ - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -52,8 +56,13 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 { return retVal; } + @Bean + public ModelConfig modelConfig() { + return daoConfig().getModelConfig(); + } + @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("HAPI_PU"); @@ -90,11 +99,10 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 { return retVal; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); return retVal; } - } diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigR4.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigR4.java index 3012865d606..433be06e184 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigR4.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigR4.java @@ -42,7 +42,7 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 { /** * Configure FHIR properties around the the JPA server via this bean */ - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -53,7 +53,7 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("HAPI_PU"); @@ -90,7 +90,7 @@ public class FhirServerConfigR4 extends BaseJavaConfigR4 { return retVal; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 99272934e9b..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-SNAPSHOT + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 617b873c042..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-SNAPSHOT + 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 9cc062b26f6..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-SNAPSHOT + 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 0b447575917..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 @@ -20,49 +20,11 @@ package ca.uhn.fhir.rest.client.impl; * #L% */ -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import ca.uhn.fhir.rest.api.CacheControlDirective; -import ca.uhn.fhir.util.XmlDetectionUtil; -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.IBase; -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 org.hl7.fhir.instance.model.api.IPrimitiveType; - -import ca.uhn.fhir.context.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementDefinition; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.*; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.api.SummaryEnum; -import ca.uhn.fhir.rest.client.api.IClientInterceptor; -import ca.uhn.fhir.rest.client.api.IHttpClient; -import ca.uhn.fhir.rest.client.api.IHttpRequest; -import ca.uhn.fhir.rest.client.api.IHttpResponse; -import ca.uhn.fhir.rest.client.api.IRestfulClient; -import ca.uhn.fhir.rest.client.api.IRestfulClientFactory; -import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.api.*; +import ca.uhn.fhir.rest.client.api.*; import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException; import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; @@ -71,8 +33,21 @@ 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.XmlUtil; +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.*; +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 { @@ -86,16 +61,17 @@ public abstract class BaseClient implements IRestfulClient { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseClient.class); private final IHttpClient myClient; + private final RestfulClientFactory myFactory; + private final String myUrlBase; private boolean myDontValidateConformance; private EncodingEnum myEncoding = null; // default unspecified (will be XML) - private final RestfulClientFactory myFactory; private List myInterceptors = new ArrayList(); private boolean myKeepResponses = false; private IHttpResponse myLastResponse; private String myLastResponseBody; private Boolean myPrettyPrint = false; private SummaryEnum mySummary; - private final String myUrlBase; + private RequestFormatParamStyleEnum myRequestFormatParamStyle = RequestFormatParamStyleEnum.SHORT; BaseClient(IHttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) { super(); @@ -118,13 +94,17 @@ public abstract class BaseClient implements IRestfulClient { } - protected Map> createExtraParams() { - HashMap> retVal = new LinkedHashMap>(); + protected Map> createExtraParams(String theCustomAcceptHeader) { + HashMap> retVal = new LinkedHashMap<>(); - 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")); + } + } } if (isPrettyPrint()) { @@ -138,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() { @@ -150,6 +130,17 @@ public abstract class BaseClient implements IRestfulClient { return myEncoding; } + /** + * Sets the encoding that will be used on requests. Default is null, which means the client will not + * explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In + * this case, the server will choose which encoding to return, and the client can handle either XML or JSON) + */ + @Override + public void setEncoding(EncodingEnum theEncoding) { + myEncoding = theEncoding; + // return this; + } + /** * {@inheritDoc} */ @@ -192,20 +183,31 @@ public abstract class BaseClient implements IRestfulClient { return mySummary; } + @Override + public void setSummary(SummaryEnum theSummary) { + mySummary = theSummary; + } + public String getUrlBase() { return myUrlBase; } + @Override + public void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle) { + Validate.notNull(theRequestFormatParamStyle, "theRequestFormatParamStyle must not be null"); + myRequestFormatParamStyle = theRequestFormatParamStyle; + } + T invokeClient(FhirContext theContext, IClientResponseHandler binding, BaseHttpClientInvocation clientInvocation) { return invokeClient(theContext, binding, clientInvocation, false); } 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); @@ -216,13 +218,15 @@ 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 (theEncoding == EncodingEnum.XML) { - params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); - } else if (theEncoding == EncodingEnum.JSON) { - params.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); + if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT && isBlank(theCustomAcceptHeader)) { + if (theEncoding == EncodingEnum.XML) { + params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); + } else if (theEncoding == EncodingEnum.JSON) { + params.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); + } } } @@ -247,12 +251,17 @@ 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()); addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_STORE, theCacheControlDirective.isNoStore()); if (theCacheControlDirective.getMaxResults() != null) { - addToCacheControlHeader(b, Constants.CACHE_CONTROL_MAX_RESULTS+"="+ Integer.toString(theCacheControlDirective.getMaxResults().intValue()), true); + addToCacheControlHeader(b, Constants.CACHE_CONTROL_MAX_RESULTS + "=" + Integer.toString(theCacheControlDirective.getMaxResults().intValue()), true); } if (b.length() > 0) { httpRequest.addHeader(Constants.HEADER_CACHE_CONTROL, b.toString()); @@ -288,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(); @@ -333,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) { @@ -397,6 +399,13 @@ public abstract class BaseClient implements IRestfulClient { return myKeepResponses; } + /** + * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! + */ + public void setKeepResponses(boolean theKeepResponses) { + myKeepResponses = theKeepResponses; + } + /** * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other @@ -406,6 +415,17 @@ public abstract class BaseClient implements IRestfulClient { return Boolean.TRUE.equals(myPrettyPrint); } + /** + * Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note + * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other + * servers which might implement it). + */ + @Override + public void setPrettyPrint(Boolean thePrettyPrint) { + myPrettyPrint = thePrettyPrint; + // return this; + } + private void keepResponseAndLogIt(boolean theLogRequestAndResponse, IHttpResponse response, String responseString) { if (myKeepResponses) { myLastResponse = response; @@ -438,56 +458,54 @@ public abstract class BaseClient implements IRestfulClient { myDontValidateConformance = theDontValidateConformance; } - /** - * Sets the encoding that will be used on requests. Default is null, which means the client will not - * explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In - * this case, the server will choose which encoding to return, and the client can handle either XML or JSON) - */ - @Override - public void setEncoding(EncodingEnum theEncoding) { - myEncoding = theEncoding; - // return this; - } - - /** - * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! - */ - public void setKeepResponses(boolean theKeepResponses) { - myKeepResponses = theKeepResponses; - } - - /** - * Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note - * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other - * servers which might implement it). - */ - @Override - public void setPrettyPrint(Boolean thePrettyPrint) { - myPrettyPrint = thePrettyPrint; - // return this; - } - - @Override - public void setSummary(SummaryEnum theSummary) { - mySummary = theSummary; - } - @Override public void unregisterInterceptor(IClientInterceptor theInterceptor) { Validate.notNull(theInterceptor, "Interceptor can not be null"); myInterceptors.remove(theInterceptor); } - static ArrayList> toTypeList(Class thePreferResponseType) { - ArrayList> preferResponseTypes = null; - if (thePreferResponseType != null) { - preferResponseTypes = new ArrayList>(1); - preferResponseTypes.add(thePreferResponseType); + 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; } - return preferResponseTypes; + } - protected final class ResourceResponseHandler implements IClientResponseHandler { + protected class ResourceResponseHandler implements IClientResponseHandler { private boolean myAllowHtmlResponse; private IIdType myId; @@ -522,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); @@ -543,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"); @@ -555,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); } @@ -563,9 +581,19 @@ public abstract class BaseClient implements IRestfulClient { return (T) instance; } - public void setPreferResponseTypes(List> thePreferResponseTypes) { + public ResourceResponseHandler setPreferResponseTypes(List> thePreferResponseTypes) { myPreferResponseTypes = thePreferResponseTypes; + return this; } } + static ArrayList> toTypeList(Class thePreferResponseType) { + ArrayList> preferResponseTypes = null; + if (thePreferResponseType != null) { + preferResponseTypes = new ArrayList>(1); + preferResponseTypes.add(thePreferResponseType); + } + return preferResponseTypes; + } + } 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 991e87f6907..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) { @@ -1276,7 +1259,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings("unchecked") @Override - public IOperationUntypedWithInput withNoParameters(Class theOutputParameterType) { + public IOperationUntypedWithInputAndPartialOutput withNoParameters(Class theOutputParameterType) { Validate.notNull(theOutputParameterType, "theOutputParameterType may not be null"); RuntimeResourceDefinition def = myContext.getResourceDefinition(theOutputParameterType); if (def == null) { @@ -1307,9 +1290,10 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings({"unchecked"}) @Override - public IOperationUntypedWithInput withParameters(IBaseParameters theParameters) { + public IOperationUntypedWithInputAndPartialOutput withParameters(IBaseParameters theParameters) { Validate.notNull(theParameters, "theParameters can not be null"); myParameters = theParameters; + myParametersDef = myContext.getResourceDefinition(theParameters.getClass()); return this; } @@ -1330,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) { @@ -1343,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; @@ -1367,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) { @@ -1383,6 +1385,8 @@ public class GenericClient extends BaseClient implements IGenericClient { } } + response.setResponseHeaders(theHeaders); + return response; } } @@ -1445,7 +1449,7 @@ public class GenericClient extends BaseClient implements IGenericClient { OutcomeResponseHandler binding = new OutcomeResponseHandler(myPrefer); - Map> params = new HashMap>(); + Map> params = new HashMap<>(); return invoke(params, binding, invocation); } @@ -1510,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 @@ -1635,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(); @@ -1936,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); } } @@ -2250,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 d6516f6c754..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-SNAPSHOT + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -16,14 +16,14 @@ ca.uhn.hapi.fhir hapi-fhir-base - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-server - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT true @@ -35,43 +35,43 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu2 - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-structures-hl7org-dstu2 - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-structures-dstu2.1 - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-structures-dstu3 - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-structures-r4 - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu2 - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT true ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu3 - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT true diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index 629d1a1b7f5..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-SNAPSHOT + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-igpacks/pom.xml b/hapi-fhir-igpacks/pom.xml index e47f70757d0..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-SNAPSHOT + 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 795e3b13164..b8bde792492 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-SNAPSHOT + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -76,8 +76,23 @@ hapi-fhir-client-okhttp ${project.version} + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-subscription + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-searchparam + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-model + ${project.version} + - + javax.mail javax.mail-api @@ -303,6 +318,9 @@ hapi-fhir-structures-hl7org-dstu2/target/jacoco.exec hapi-fhir-structures-dstu3/target/jacoco.exec hapi-fhir-structures-r4/target/jacoco.exec + hapi-fhir-jpaserver-model/target/jacoco.exec + hapi-fhir-jpaserver-searchparam/target/jacoco.exec + hapi-fhir-jpaserver-subscription/target/jacoco.exec hapi-fhir-jpaserver-base/target/jacoco.exec hapi-fhir-client-okhttp/target/jacoco.exec hapi-fhir-android/target/jacoco.exec @@ -341,6 +359,9 @@ ../hapi-fhir-structures-hl7org-dstu2/src/test/java ../hapi-fhir-structures-dstu3/src/test/java ../hapi-fhir-structures-r4/src/test/java + ../hapi-fhir-jpaserver-model/src/main/java + ../hapi-fhir-jpaserver-searchparam/src/main/java + ../hapi-fhir-jpaserver-subscription/src/main/java ../hapi-fhir-jpaserver-base/src/main/java ../hapi-fhir-client-okhttp/src/main/java @@ -368,6 +389,9 @@ ../hapi-fhir-base/src/main/java ../hapi-fhir-client/src/main/java ../hapi-fhir-server/src/main/java + ../hapi-fhir-jpaserver-model/src/main/java + ../hapi-fhir-jpaserver-searchparam/src/main/java + ../hapi-fhir-jpaserver-subscription/src/main/java ../hapi-fhir-jpaserver-base/src/main/java @@ -395,6 +419,15 @@ ../hapi-fhir-base/src/test/resources + + ../hapi-fhir-jpaserver-model/src/test/resources + + + ../hapi-fhir-jpaserver-searchparam/src/test/resources + + + ../hapi-fhir-jpaserver-subscription/src/test/resources + ../hapi-fhir-jpaserver-base/src/test/resources diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index 798d8479048..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-SNAPSHOT + 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-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java index 182421e83c0..a79058e12d7 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java @@ -22,6 +22,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * #L% */ import java.io.*; +import java.util.List; import java.util.Map.Entry; import javax.ws.rs.core.MediaType; @@ -104,8 +105,11 @@ public class JaxRsResponse extends RestfulResponse { private ResponseBuilder buildResponse(int statusCode) { ResponseBuilder response = Response.status(statusCode); - for (Entry header : getHeaders().entrySet()) { - response.header(header.getKey(), header.getValue()); + for (Entry> header : getHeaders().entrySet()) { + final String key = header.getKey(); + for (String value : header.getValue()) { + response.header(key, value); + } } return response; } diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java index b9979da676b..c5b58275855 100644 --- a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jaxrs.server.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -10,6 +11,7 @@ import java.util.Set; import javax.ws.rs.core.Response; +import org.hamcrest.Matchers; import org.hl7.fhir.instance.model.api.IBaseBinary; import org.junit.Before; import org.junit.Test; @@ -108,10 +110,24 @@ public class JaxRsResponseTest { assertEquals("application/xml+fhir; charset=UTF-8", result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); } + @Test + public void addMultipleHeaderValues() throws IOException { + response.addHeader("Authorization", "Basic"); + response.addHeader("Authorization", "Bearer"); + response.addHeader("Cache-Control", "no-cache, no-store"); + + final IBaseBinary binary = new Binary(); + binary.setContentType("abc"); + binary.setContent(new byte[] { 1 }); + final Response result = (Response) RestfulServerUtils.streamResponseAsResource(request.getServer(), binary, theSummaryMode, 200, false, false, this.request); + + assertThat(result.getHeaders().get("Authorization"), Matchers.contains("Basic", "Bearer")); + assertThat(result.getHeaders().get("Cache-Control"), Matchers.contains("no-cache, no-store")); + } + private Patient createPatient() { Patient theResource = new Patient(); theResource.setId(new IdDt(15L)); return theResource; } - } diff --git a/hapi-fhir-jaxrsserver-example/pom.xml b/hapi-fhir-jaxrsserver-example/pom.xml index 9b9b3dcc984..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-SNAPSHOT + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 7f3e7bf46bf..6f662773851 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-SNAPSHOT + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -70,6 +70,21 @@ hapi-fhir-server ${project.version} + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-subscription + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-searchparam + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-model + ${project.version} + ca.uhn.hapi.fhir hapi-fhir-validation @@ -163,7 +178,7 @@ org.thymeleaf - thymeleaf-spring4 + thymeleaf-spring5 @@ -550,74 +565,29 @@ - de.juplo - hibernate-maven-plugin + de.jpdigital + hibernate52-ddl-maven-plugin + + + derby_10_7 + postgresql92 + mysql57 + mariadb + oracle12c + sqlserver2012 + + ${project.build.directory}/classes/ca/uhn/hapi/fhir/jpa/docs/database + + ca.uhn.fhir.jpa.entity + ca.uhn.fhir.jpa.model.entity + + - derby107 process-classes - create + gen-ddl - - org.hibernate.dialect.DerbyTenSevenDialect - ${project.build.directory}/classes/ca/uhn/hapi/fhir/jpa/docs/database/persistence_create_derby107.sql - - - - postgres94 - process-classes - - create - - - org.hibernate.dialect.PostgreSQL94Dialect - ${project.build.directory}/classes/ca/uhn/hapi/fhir/jpa/docs/database/persistence_create_postgres94.sql - - - - mysql57 - process-classes - - create - - - org.hibernate.dialect.MySQL57Dialect - ${project.build.directory}/classes/ca/uhn/hapi/fhir/jpa/docs/database/persistence_create_mysql57.sql - - - - mariadb103 - process-classes - - create - - - org.hibernate.dialect.MariaDB103Dialect - ${project.build.directory}/classes/ca/uhn/hapi/fhir/jpa/docs/database/persistence_create_mariadb103.sql - - - - oracle12c - process-classes - - create - - - org.hibernate.dialect.Oracle12cDialect - ${project.build.directory}/classes/ca/uhn/hapi/fhir/jpa/docs/database/persistence_create_oracle12c.sql - - - - sqlserver2012 - process-classes - - create - - - org.hibernate.dialect.SQLServer2012Dialect - ${project.build.directory}/classes/ca/uhn/hapi/fhir/jpa/docs/database/persistence_create_sqlserver2012.sql - @@ -670,7 +640,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 3aa4f830363..18e151eb601 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,38 @@ package ca.uhn.fhir.jpa.config; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.HapiLocalizer; +import ca.uhn.fhir.jpa.dao.DatabaseSearchParamProvider; +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.searchparam.registry.ISearchParamProvider; +import ca.uhn.fhir.jpa.subscription.config.BaseSubscriptionConfig; +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,49 +53,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.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 ca.uhn.fhir.jpa.util.IReindexController; -import ca.uhn.fhir.jpa.util.ReindexController; -import org.hibernate.cfg.AvailableSettings; -import org.hibernate.jpa.HibernatePersistenceProvider; -import org.hibernate.query.criteria.LiteralHandlingMode; -import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; -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.Map; -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"; @@ -70,11 +68,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()); @@ -91,44 +84,7 @@ public abstract class BaseConfig implements SchedulingConfigurer { * factory with HAPI FHIR customizations */ protected LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean() { - @Override - public Map getJpaPropertyMap() { - Map retVal = super.getJpaPropertyMap(); - - if (!retVal.containsKey(AvailableSettings.CRITERIA_LITERAL_HANDLING_MODE)) { - retVal.put(AvailableSettings.CRITERIA_LITERAL_HANDLING_MODE, LiteralHandlingMode.BIND); - } - - if (!retVal.containsKey(AvailableSettings.CONNECTION_HANDLING)) { - retVal.put(AvailableSettings.CONNECTION_HANDLING, PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_HOLD); - } - - /* - * Set some performance options - */ - - if (!retVal.containsKey(AvailableSettings.STATEMENT_BATCH_SIZE)) { - retVal.put(AvailableSettings.STATEMENT_BATCH_SIZE, "30"); - } - - if (!retVal.containsKey(AvailableSettings.ORDER_INSERTS)) { - retVal.put(AvailableSettings.ORDER_INSERTS, "true"); - } - - if (!retVal.containsKey(AvailableSettings.ORDER_UPDATES)) { - retVal.put(AvailableSettings.ORDER_UPDATES, "true"); - } - - if (!retVal.containsKey(AvailableSettings.BATCH_VERSIONED_DATA)) { - retVal.put(AvailableSettings.BATCH_VERSIONED_DATA, "true"); - } - - return retVal; - } - - - }; + LocalContainerEntityManagerFactoryBean retVal = new HapiFhirLocalContainerEntityManagerFactoryBean(); configureEntityManagerFactory(retVal, fhirContext()); return retVal; } @@ -136,54 +92,50 @@ 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 IReindexController reindexController() { - return new ReindexController(); - } - - @Bean() - public ScheduledExecutorService scheduledExecutorService() { + public ScheduledExecutorFactoryBean scheduledExecutorService() { ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean(); b.setPoolSize(5); b.afterPropertiesSet(); - return b.getObject(); + return b; } - @Bean(name="mySubscriptionTriggeringProvider") + @Bean(name = "mySubscriptionTriggeringProvider") @Lazy public SubscriptionTriggeringProvider subscriptionTriggeringProvider() { 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(); } + @Bean + protected ISearchParamProvider searchParamProvider() { + return new DatabaseSearchParamProvider(); + } + /** * Note: If you're going to use this, you need to provide a bean * of type {@link ca.uhn.fhir.jpa.subscription.email.IEmailSender} @@ -207,30 +159,14 @@ 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; - } public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) { theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer())); - theFactory.setPackagesToScan("ca.uhn.fhir.jpa.entity"); + theFactory.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity"); theFactory.setPersistenceProvider(new HibernatePersistenceProvider()); } 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/config/BaseDstu2Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java index 9da25a9f395..f0ca4a9eb7d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java @@ -1,7 +1,12 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; +import ca.uhn.fhir.jpa.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu2; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu2; import ca.uhn.fhir.jpa.term.HapiTerminologySvcDstu2; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.jpa.util.ResourceCountCache; @@ -111,7 +116,7 @@ public class BaseDstu2Config extends BaseConfig { @Bean public ISearchParamRegistry searchParamRegistry() { - return new SearchParamRegistryDstu2(); + return new SearchParamRegistryDstu2(searchParamProvider()); } @Bean(name = "mySystemDaoDstu2", autowire = Autowire.BY_NAME) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java index ae4fbc01409..36c03a087c7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java @@ -21,8 +21,8 @@ package ca.uhn.fhir.jpa.config; */ import ca.uhn.fhir.i18n.HapiLocalizer; -import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; -import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import org.hibernate.HibernateException; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirLocalContainerEntityManagerFactoryBean.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirLocalContainerEntityManagerFactoryBean.java new file mode 100644 index 00000000000..e31adf6bbc4 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirLocalContainerEntityManagerFactoryBean.java @@ -0,0 +1,71 @@ +package ca.uhn.fhir.jpa.config; + +/*- + * #%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.hibernate.cfg.AvailableSettings; +import org.hibernate.query.criteria.LiteralHandlingMode; +import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; + +import java.util.Map; + +/** + * This class is an extension of the Spring/Hibernate LocalContainerEntityManagerFactoryBean + * that sets some sensible default property values + */ +public class HapiFhirLocalContainerEntityManagerFactoryBean extends LocalContainerEntityManagerFactoryBean { + @Override + public Map getJpaPropertyMap() { + Map retVal = super.getJpaPropertyMap(); + + if (!retVal.containsKey(AvailableSettings.CRITERIA_LITERAL_HANDLING_MODE)) { + retVal.put(AvailableSettings.CRITERIA_LITERAL_HANDLING_MODE, LiteralHandlingMode.BIND); + } + + if (!retVal.containsKey(AvailableSettings.CONNECTION_HANDLING)) { + retVal.put(AvailableSettings.CONNECTION_HANDLING, PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_HOLD); + } + + /* + * Set some performance options + */ + + if (!retVal.containsKey(AvailableSettings.STATEMENT_BATCH_SIZE)) { + retVal.put(AvailableSettings.STATEMENT_BATCH_SIZE, "30"); + } + + if (!retVal.containsKey(AvailableSettings.ORDER_INSERTS)) { + retVal.put(AvailableSettings.ORDER_INSERTS, "true"); + } + + if (!retVal.containsKey(AvailableSettings.ORDER_UPDATES)) { + retVal.put(AvailableSettings.ORDER_UPDATES, "true"); + } + + if (!retVal.containsKey(AvailableSettings.BATCH_VERSIONED_DATA)) { + retVal.put(AvailableSettings.BATCH_VERSIONED_DATA, "true"); + } + + return retVal; + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java index 2381a0bcf15..22eb20d0a8b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java @@ -3,11 +3,15 @@ package ca.uhn.fhir.jpa.config.dstu3; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.jpa.config.BaseConfig; -import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; +import ca.uhn.fhir.jpa.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; +import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.dstu3.TransactionProcessorVersionAdapterDstu3; -import ca.uhn.fhir.jpa.dao.dstu3.SearchParamExtractorDstu3; -import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3; import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu3; import ca.uhn.fhir.jpa.term.HapiTerminologySvcDstu3; import ca.uhn.fhir.jpa.term.IHapiTerminologyLoaderSvc; import ca.uhn.fhir.jpa.term.IHapiTerminologySvcDstu3; @@ -119,7 +123,7 @@ public class BaseDstu3Config extends BaseConfig { @Bean public ISearchParamRegistry searchParamRegistry() { - return new SearchParamRegistryDstu3(); + return new SearchParamRegistryDstu3(searchParamProvider()); } @Bean(name = "mySystemDaoDstu3", autowire = Autowire.BY_NAME) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java index 979f202f2bb..fdc4e637a78 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java @@ -3,12 +3,16 @@ package ca.uhn.fhir.jpa.config.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.jpa.config.BaseConfig; -import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.dao.r4.SearchParamExtractorR4; -import ca.uhn.fhir.jpa.dao.r4.SearchParamRegistryR4; +import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; +import ca.uhn.fhir.jpa.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; +import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4; import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryR4; import ca.uhn.fhir.jpa.term.HapiTerminologySvcR4; import ca.uhn.fhir.jpa.term.IHapiTerminologyLoaderSvc; import ca.uhn.fhir.jpa.term.IHapiTerminologySvcR4; @@ -134,7 +138,7 @@ public class BaseR4Config extends BaseConfig { @Bean public ISearchParamRegistry searchParamRegistry() { - return new SearchParamRegistryR4(); + return new SearchParamRegistryR4(searchParamProvider()); } @Bean(name = "mySystemDaoR4", autowire = Autowire.BY_NAME) 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 d369de228f2..dc3213931af 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,151 +1,24 @@ 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 java.util.stream.Collectors; - -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 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.data.IForcedIdDao; -import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; -import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTagDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamCoordsDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamDateDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamNumberDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamQuantityDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamStringDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao; -import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; -import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao; -import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; -import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; -import ca.uhn.fhir.jpa.dao.data.ISearchDao; -import ca.uhn.fhir.jpa.dao.index.IndexingSupport; -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.context.*; +import ca.uhn.fhir.jpa.dao.data.*; +import ca.uhn.fhir.jpa.dao.index.*; +import ca.uhn.fhir.jpa.model.entity.*; +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.searchparam.ResourceMetaParams; +import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; +import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper; +import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; 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; @@ -160,26 +33,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.PreconditionFailedException; -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; @@ -188,6 +46,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 @@ -211,7 +108,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; @@ -219,48 +116,20 @@ public abstract class BaseHapiFhirDao implements IDao, public static final String OO_SEVERITY_ERROR = "error"; 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; @Autowired(required = false) protected IFulltextSearchSvc myFulltextSearchSvc; @Autowired() @@ -269,6 +138,8 @@ public abstract class BaseHapiFhirDao implements IDao, protected IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao; @Autowired() protected IResourceIndexedSearchParamTokenDao myResourceIndexedSearchParamTokenDao; + @Autowired + protected IResourceLinkDao myResourceLinkDao; @Autowired() protected IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao; @Autowired() @@ -293,6 +164,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; @@ -304,25 +177,31 @@ 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; + @Autowired + private BeanFactory beanFactory; + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired + private SearchParamExtractorService mySearchParamExtractorService; + @Autowired + private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor; + @Autowired + private DatabaseSearchParamSynchronizer myDatabaseSearchParamSynchronizer; + private ApplicationContext myApplicationContext; - private Map, IFhirResourceDao> myResourceTypeToDao; - public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { - if (theRequestDetails != null) { - theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST); - } - } - - protected void createForcedIdIfNeeded(ResourceTable theEntity, IIdType theId) { - if (theId.isEmpty() == false && theId.hasIdPart()) { - if (isValidPid(theId)) { - return; + /** + * Returns the newly created forced ID. If the entity already had a forced ID, or if + * none was created, returns null. + */ + protected ForcedId createForcedIdIfNeeded(ResourceTable theEntity, IIdType theId, boolean theCreateForPureNumericIds) { + if (theId.isEmpty() == false && theId.hasIdPart() && theEntity.getForcedId() == null) { + if (!theCreateForPureNumericIds && IdHelperService.isValidPid(theId)) { + return null; } ForcedId fid = new ForcedId(); @@ -330,11 +209,15 @@ public abstract class BaseHapiFhirDao implements IDao, fid.setForcedId(theId.getIdPart()); fid.setResource(theEntity); theEntity.setForcedId(fid); + return fid; } + + return null; } 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()) { @@ -373,6 +256,19 @@ public abstract class BaseHapiFhirDao implements IDao, } }); + /* + * 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 + */ + 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(nextPartition); + return null; + }); + } + /* * Delete historical versions */ @@ -416,6 +312,7 @@ public abstract class BaseHapiFhirDao implements IDao, } } }); + for (Long next : historicalIds) { txTemplate.execute(t -> { expungeHistoricalVersion(next); @@ -434,7 +331,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"); @@ -517,6 +414,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(); @@ -525,7 +424,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); @@ -556,7 +455,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) { @@ -643,7 +541,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); } @@ -655,6 +553,7 @@ public abstract class BaseHapiFhirDao implements IDao, } } } + protected void flushJpaSession() { SessionImpl session = (SessionImpl) myEntityManager.unwrap(Session.class); int insertionCount = session.getActionQueue().numberOfInsertions(); @@ -675,7 +574,7 @@ public abstract class BaseHapiFhirDao implements IDao, return retVal; } - public DaoConfig getConfig() { + protected DaoConfig getConfig() { return myConfig; } @@ -707,62 +606,11 @@ public abstract class BaseHapiFhirDao implements IDao, @SuppressWarnings("unchecked") public IFhirResourceDao getDao(Class theType) { - Map, IFhirResourceDao> resourceTypeToDao = getDaos(); - IFhirResourceDao dao = (IFhirResourceDao) resourceTypeToDao.get(theType); - return dao; + return myDaoRegistry.getResourceDaoIfExists(theType); } protected IFhirResourceDao getDaoOrThrowException(Class theClass) { - IFhirResourceDao retVal = getDao(theClass); - if (retVal == null) { - List supportedResourceTypes = getDaos() - .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 " + getContext().getResourceDefinition(theClass).getName() + " - Can handle: " + supportedResourceTypes); - } - return retVal; - } - - 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(); + return myDaoRegistry.getDaoOrThrowException(theClass); } protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { @@ -901,37 +749,12 @@ public abstract class BaseHapiFhirDao implements IDao, } 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.getModelConfig(), 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) { @@ -951,34 +774,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()) { @@ -1013,7 +808,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); @@ -1100,7 +895,7 @@ public abstract class BaseHapiFhirDao implements IDao, } @SuppressWarnings("unchecked") - private R populateResourceMetadataHapi(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation, IResource res) { + private R populateResourceMetadataHapi(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation, IResource res, Long theVersion) { R retVal = (R) res; if (theEntity.getDeleted() != null) { res = (IResource) myContext.getResourceDefinition(theResourceType).newInstance(); @@ -1122,7 +917,7 @@ public abstract class BaseHapiFhirDao implements IDao, } } - res.setId(theEntity.getIdDt()); + res.setId(theEntity.getIdDt().withVersion(theVersion.toString())); ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion())); ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished()); @@ -1166,7 +961,7 @@ public abstract class BaseHapiFhirDao implements IDao, } @SuppressWarnings("unchecked") - private R populateResourceMetadataRi(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation, IAnyResource res) { + private R populateResourceMetadataRi(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation, IAnyResource res, Long theVersion) { R retVal = (R) res; if (theEntity.getDeleted() != null) { res = (IAnyResource) myContext.getResourceDefinition(theResourceType).newInstance(); @@ -1195,6 +990,7 @@ public abstract class BaseHapiFhirDao implements IDao, res.getMeta().setVersionId(null); populateResourceIdFromEntity(theEntity, res); + res.setId(res.getIdElement().withVersion(theVersion.toString())); res.getMeta().setLastUpdated(theEntity.getUpdatedDate()); IDao.RESOURCE_PID.put(res, theEntity.getId()); @@ -1254,25 +1050,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(""); @@ -1290,7 +1067,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. *

@@ -1341,11 +1117,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()); @@ -1366,26 +1137,36 @@ public abstract class BaseHapiFhirDao implements IDao, byte[] resourceBytes = null; ResourceEncodingEnum resourceEncoding = null; Collection myTagList = null; + Long version = null; if (theEntity instanceof ResourceHistoryTable) { ResourceHistoryTable history = (ResourceHistoryTable) theEntity; resourceBytes = history.getResource(); resourceEncoding = history.getEncoding(); myTagList = history.getTags(); + version = history.getVersion(); } else if (theEntity instanceof ResourceTable) { ResourceTable resource = (ResourceTable) theEntity; - ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion()); - if (history == null) { - return null; + version = theEntity.getVersion(); + ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), version); + while (history == null) { + if (version > 1L) { + version--; + history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), version); + } else { + return null; + } } resourceBytes = history.getResource(); resourceEncoding = history.getEncoding(); myTagList = resource.getTags(); + version = history.getVersion(); } else if (theEntity instanceof ResourceSearchView) { // This is the search View ResourceSearchView myView = (ResourceSearchView) theEntity; resourceBytes = myView.getResource(); resourceEncoding = myView.getEncoding(); + version = myView.getVersion(); if (theTagList == null) myTagList = new HashSet<>(); else @@ -1450,16 +1231,15 @@ public abstract class BaseHapiFhirDao implements IDao, // 5. fill MetaData if (retVal instanceof IResource) { IResource res = (IResource) retVal; - retVal = populateResourceMetadataHapi(resourceType, theEntity, myTagList, theForHistoryOperation, res); + retVal = populateResourceMetadataHapi(resourceType, theEntity, myTagList, theForHistoryOperation, res, version); } else { IAnyResource res = (IAnyResource) retVal; - retVal = populateResourceMetadataRi(resourceType, theEntity, myTagList, theForHistoryOperation, res); + retVal = populateResourceMetadataRi(resourceType, theEntity, myTagList, theForHistoryOperation, res, version); } return retVal; } - @Override public String toResourceName(Class theResourceType) { return myContext.getResourceDefinition(theResourceType).getName(); } @@ -1473,16 +1253,6 @@ public abstract class BaseHapiFhirDao implements IDao, return new SliceImpl<>(Collections.singletonList(theVersion.getId())); } - @Override - public Long translateForcedIdToPid(String theResourceName, String theResourceId) { - return translateForcedIdToPids(new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0); - } - - protected List translateForcedIdToPids(IIdType theId) { - return translateForcedIdToPids(theId, myForcedIdDao); - } - - @SuppressWarnings("unchecked") protected ResourceTable updateEntity(RequestDetails theRequest, final IBaseResource theResource, ResourceTable theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing, @@ -1514,14 +1284,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); @@ -1537,7 +1307,8 @@ public abstract class BaseHapiFhirDao implements IDao, if (thePerformIndexing) { - newParams = new ResourceIndexedSearchParams(this, theUpdateTime, theEntity, theResource, existingParams); + newParams = new ResourceIndexedSearchParams(); + mySearchParamWithInlineReferencesExtractor.populateFromResource(newParams, this, theUpdateTime, theEntity, theResource, existingParams); changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true); @@ -1547,10 +1318,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); @@ -1623,7 +1394,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); } @@ -1642,9 +1412,9 @@ public abstract class BaseHapiFhirDao implements IDao, * Indexing */ if (thePerformIndexing) { - newParams.removeCommon(theEntity, existingParams); - - } // if thePerformIndexing + myDatabaseSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, theEntity, existingParams); + mySearchParamWithInlineReferencesExtractor.storeCompositeStringUniques(newParams, theEntity, existingParams); + } if (theResource != null) { populateResourceIdFromEntity(theEntity, theResource); @@ -1663,7 +1433,7 @@ public abstract class BaseHapiFhirDao implements IDao, boolean theForceUpdateVersion, RequestDetails theRequestDetails, ResourceTable theEntity, IIdType theResourceId, IBaseResource theOldResource) { // Notify interceptors - ActionRequestDetails requestDetails = null; + ActionRequestDetails requestDetails; if (theRequestDetails != null) { requestDetails = new ActionRequestDetails(theRequestDetails, theResource, theResourceId.getResourceType(), theResourceId); notifyInterceptors(RestOperationTypeEnum.UPDATE, requestDetails); @@ -1843,6 +1613,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) { @@ -1882,70 +1696,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()); - - /* - * The following block of code is used to strip out diacritical marks from latin script - * and also convert to upper case. E.g. "j?mes" becomes "JAMES". - * - * See http://www.unicode.org/charts/PDF/U0300.pdf for the logic - * behind stripping 0300-036F - * - * See #454 for an issue where we were completely stripping non latin characters - * See #832 for an issue where we normalize korean characters, which are decomposed - */ - String string = Normalizer.normalize(theString, Normalizer.Form.NFD); - for (int i = 0, n = string.length(); i < n; ++i) { - char c = string.charAt(i); - if (c >= '\u0300' && c <= '\u036F') { - continue; - } else { - outBuffer.append(c); - } - } - - return new String(outBuffer.toCharArray()).toUpperCase(); - } - private static String parseNarrativeTextIntoWords(IBaseResource theResource) { StringBuilder b = new StringBuilder(); @@ -2002,169 +1752,10 @@ public abstract class BaseHapiFhirDao implements IDao, return retVal; } - protected static Long translateForcedIdToPid(String theResourceName, String theResourceId, IForcedIdDao - theForcedIdDao) { - return translateForcedIdToPids(new IdDt(theResourceName, theResourceId), theForcedIdDao).get(0); - } - - static List translateForcedIdToPids(IIdType theId, IForcedIdDao theForcedIdDao) { - Validate.isTrue(theId.hasIdPart()); - - if (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 d5cc0ce599c..56476f422fd 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,14 +25,16 @@ 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.dao.r4.MatchResourceUrlService; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 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.IReindexController; import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils; import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils; import ca.uhn.fhir.model.api.*; @@ -42,7 +44,6 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.param.QualifierDetails; -import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; @@ -54,16 +55,12 @@ import org.hl7.fhir.instance.model.api.*; import org.hl7.fhir.r4.model.InstantType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Required; -import org.springframework.lang.NonNull; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; -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.TransactionTemplate; -import javax.annotation.Nonnull; import javax.annotation.PostConstruct; import javax.persistence.NoResultException; import javax.persistence.TypedQuery; @@ -80,19 +77,13 @@ public abstract class BaseHapiFhirResourceDao extends B protected PlatformTransactionManager myPlatformTransactionManager; @Autowired(required = false) protected IFulltextSearchSvc mySearchDao; - @Autowired() - protected ISearchResultDao mySearchResultDao; @Autowired protected DaoConfig myDaoConfig; - @Autowired - private IResourceLinkDao myResourceLinkDao; private String myResourceName; private Class myResourceType; private String mySecondaryPrimaryKeyParamName; @Autowired - private ISearchParamRegistry mySearchParamRegistry; - @Autowired - private IReindexController myReindexController; + private MatchResourceUrlService myMatchResourceUrlService; @Override public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { @@ -279,7 +270,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 = myMatchResourceUrlService.processMatchUrl(theUrl, myResourceType); if (resource.size() > 1) { if (myDaoConfig.isAllowMultipleDelete() == false) { throw new PreconditionFailedException(getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resource.size())); @@ -379,7 +370,7 @@ public abstract class BaseHapiFhirResourceDao extends B entity.setResourceType(toResourceName(theResource)); if (isNotBlank(theIfNoneExist)) { - Set match = processMatchUrl(theIfNoneExist, myResourceType); + Set match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType); if (match.size() > 1) { String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size()); throw new PreconditionFailedException(msg); @@ -390,12 +381,26 @@ public abstract class BaseHapiFhirResourceDao extends B } } + boolean serverAssignedId; if (isNotBlank(theResource.getIdElement().getIdPart())) { - if (isValidPid(theResource.getIdElement())) { - throw new UnprocessableEntityException( - "This server cannot create an entity with a user-specified numeric ID - Client should not specify an ID when creating a new resource, or should include at least one letter in the ID to force a client-defined ID"); + switch (myDaoConfig.getResourceClientIdStrategy()) { + case NOT_ALLOWED: + throw new ResourceNotFoundException( + getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedIdNotAllowed", theResource.getIdElement().getIdPart())); + case ALPHANUMERIC: + if (theResource.getIdElement().isIdPartValidLong()) { + throw new InvalidRequestException( + getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart())); + } + createForcedIdIfNeeded(entity, theResource.getIdElement(), false); + break; + case ANY: + createForcedIdIfNeeded(entity, theResource.getIdElement(), true); + break; } - createForcedIdIfNeeded(entity, theResource.getIdElement()); + serverAssignedId = false; + } else { + serverAssignedId = true; } // Notify interceptors @@ -416,8 +421,21 @@ public abstract class BaseHapiFhirResourceDao extends B // Perform actual DB update ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing); - theResource.setId(entity.getIdDt()); + theResource.setId(entity.getIdDt()); + if (serverAssignedId) { + switch (myDaoConfig.getResourceClientIdStrategy()) { + case NOT_ALLOWED: + case ALPHANUMERIC: + break; + case ANY: + ForcedId forcedId = createForcedIdIfNeeded(updatedEntity, theResource.getIdElement(), true); + if (forcedId != null) { + myForcedIdDao.save(forcedId); + } + break; + } + } /* * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction), @@ -618,28 +636,27 @@ public abstract class BaseHapiFhirResourceDao extends B } if (myDaoConfig.isMarkResourcesForReindexingUponSearchParameterChange()) { - if (isNotBlank(theExpression)) { + if (isNotBlank(theExpression) && theExpression.contains(".")) { final String resourceType = theExpression.substring(0, theExpression.indexOf('.')); ourLog.debug("Marking all resources of type {} for reindexing due to updated search parameter with path: {}", resourceType, theExpression); TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - Integer updatedCount = txTemplate.execute(new TransactionCallback() { - @Override - public @NonNull - Integer doInTransaction(@Nonnull TransactionStatus theStatus) { - return myResourceTableDao.markResourcesOfTypeAsRequiringReindexing(resourceType); - } + txTemplate.execute(t->{ + myResourceReindexingSvc.markAllResourcesForReindexing(resourceType); + return null; }); - ourLog.debug("Marked {} resources for reindexing", updatedCount); + ourLog.debug("Marked resources of type {} for reindexing", resourceType); } } mySearchParamRegistry.requestRefresh(); - myReindexController.requestReindex(); } + @Autowired + private IResourceReindexingSvc myResourceReindexingSvc; + @Override public MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequestDetails) { // Notify interceptors @@ -727,6 +744,7 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } + @SuppressWarnings("JpaQlInspection") @Override public MT metaGetOperation(Class theType, RequestDetails theRequestDetails) { // Notify interceptors @@ -773,7 +791,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 + "]"); } @@ -829,7 +847,7 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public Set processMatchUrl(String theMatchUrl) { - return processMatchUrl(theMatchUrl, getResourceType()); + return myMatchResourceUrlService.processMatchUrl(theMatchUrl, getResourceType()); } @Override @@ -858,6 +876,11 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public T read(IIdType theId, RequestDetails theRequestDetails) { + return read(theId, theRequestDetails, false); + } + + @Override + public T read(IIdType theId, RequestDetails theRequestDetails, boolean theDeletedOk) { validateResourceTypeAndThrowIllegalArgumentException(theId); // Notify interceptors @@ -873,10 +896,13 @@ public abstract class BaseHapiFhirResourceDao extends B T retVal = toResource(myResourceType, entity, null, false); - if (entity.getDeleted() != null) { - throw new ResourceGoneException("Resource was deleted at " + new InstantType(entity.getDeleted()).getValueAsString()); + if (theDeletedOk == false) { + if (entity.getDeleted() != null) { + throw new ResourceGoneException("Resource was deleted at " + new InstantType(entity.getDeleted()).getValueAsString()); + } } + ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); return retVal; } @@ -891,7 +917,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) { @@ -931,7 +957,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); } @@ -1172,7 +1198,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)) { @@ -1212,7 +1238,7 @@ public abstract class BaseHapiFhirResourceDao extends B IIdType resourceId; if (isNotBlank(theMatchUrl)) { StopWatch sw = new StopWatch(); - Set match = processMatchUrl(theMatchUrl, myResourceType); + Set match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType); if (match.size() > 1) { String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size()); throw new PreconditionFailedException(msg); @@ -1234,10 +1260,6 @@ public abstract class BaseHapiFhirResourceDao extends B try { entity = readEntityLatestVersion(resourceId); } catch (ResourceNotFoundException e) { - if (resourceId.isIdPartValidLong()) { - throw new InvalidRequestException( - getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart())); - } return doCreate(theResource, null, thePerformIndexing, new Date(), theRequestDetails); } } @@ -1253,6 +1275,19 @@ public abstract class BaseHapiFhirResourceDao extends B IBaseResource oldResource = toResource(entity, false); + /* + * Mark the entity as not deleted - This is also done in the actual updateInternal() + * method later on so it usually doesn't matter whether we do it here, but in the + * case of a transaction with multiple PUTs we don't get there until later so + * having this here means that a transaction can have a reference in one + * resource to another resource in the same transaction that is being + * un-deleted by the transaction. Wacky use case, sure. But it's real. + * + * See SystemProviderR4Test#testTransactionReSavesPreviouslyDeletedResources + * for a test that needs this. + */ + entity.setDeleted(null); + /* * If we aren't indexing, that means we're doing this inside a transaction. * The transaction will do the actual storage to the database a bit later on, @@ -1315,12 +1350,14 @@ public abstract class BaseHapiFhirResourceDao extends B private void validateGivenIdIsAppropriateToRetrieveResource(IIdType theId, BaseHasResource entity) { if (entity.getForcedId() != null) { - if (theId.isIdPartValidLong()) { - // This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning that - // as far as the outside world is concerned, the given ID doesn't exist (it's just an internal pointer - // to the - // forced ID) - throw new ResourceNotFoundException(theId); + if (myDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) { + if (theId.isIdPartValidLong()) { + // This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning that + // as far as the outside world is concerned, the given ID doesn't exist (it's just an internal pointer + // to the + // forced ID) + throw new ResourceNotFoundException(theId); + } } } } 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 70188beca0e..b3c61c7937b 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,44 +1,29 @@ 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.entity.ForcedId; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.util.ExpungeOptions; import ca.uhn.fhir.jpa.util.ExpungeOutcome; -import ca.uhn.fhir.jpa.util.ReindexFailureException; import ca.uhn.fhir.jpa.util.ResourceCountCache; 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.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.util.StopWatch; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import org.hibernate.search.util.impl.Executors; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.data.domain.PageRequest; import org.springframework.transaction.PlatformTransactionManager; -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.annotation.Nonnull; import javax.annotation.Nullable; -import javax.persistence.Query; -import java.util.*; -import java.util.concurrent.*; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.locks.ReentrantLock; -import static org.apache.commons.lang3.StringUtils.isBlank; - /* * #%L * HAPI FHIR JPA Server @@ -65,85 +50,9 @@ public abstract class BaseHapiFhirSystemDao extends BaseHapiFhirDao idsToReindex = txTemplate.execute(theStatus -> { - int maxResult = 500; - if (theCount != null) { - maxResult = Math.min(theCount, 2000); - } - maxResult = Math.max(maxResult, 10); - - ourLog.debug("Beginning indexing query with maximum {}", maxResult); - return myResourceTableDao - .findIdsOfResourcesRequiringReindexing(new PageRequest(0, maxResult)) - .getContent(); - }); - - // If no IDs need reindexing, we're good here - if (idsToReindex.isEmpty()) { - return 0; - } - - // Reindex - StopWatch sw = new StopWatch(); - - // Execute each reindex in a task within a threadpool - int threadCount = getConfig().getReindexThreadCount(); - RejectedExecutionHandler rejectHandler = new Executors.BlockPolicy(); - ThreadPoolExecutor executor = new ThreadPoolExecutor(threadCount, threadCount, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), - myReindexingThreadFactory, - rejectHandler - ); - List> futures = new ArrayList<>(); - for (Long nextId : idsToReindex) { - futures.add(executor.submit(new ResourceReindexingTask(nextId))); - } - for (Future next : futures) { - try { - next.get(); - } catch (Exception e) { - throw new InternalErrorException("Failed to reindex: ", e); - } - } - executor.shutdown(); - - ourLog.info("Reindexed {} resources in {} threads - {}ms/resource", idsToReindex.size(), threadCount, sw.getMillisPerOperation(idsToReindex.size())); - return idsToReindex.size(); - } @Override - @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.NEVER) public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions) { return doExpunge(null, null, null, theExpungeOptions); } @@ -182,165 +91,5 @@ public abstract class BaseHapiFhirSystemDao extends BaseHapiFhirDao() { - @Override - public Void doInTransaction(@Nonnull TransactionStatus theStatus) { - ourLog.info("Marking resource with PID {} as indexing_failed", new Object[] {theId}); - Query q = myEntityManager.createQuery("UPDATE ResourceTable t SET t.myIndexStatus = :status WHERE t.myId = :id"); - q.setParameter("status", INDEX_STATUS_INDEXING_FAILED); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceTag t WHERE t.myResourceId = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamCoords t WHERE t.myResourcePid = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamDate t WHERE t.myResourcePid = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamNumber t WHERE t.myResourcePid = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamQuantity t WHERE t.myResourcePid = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamString t WHERE t.myResourcePid = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamToken t WHERE t.myResourcePid = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamUri t WHERE t.myResourcePid = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceLink t WHERE t.mySourceResourcePid = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - q = myEntityManager.createQuery("DELETE FROM ResourceLink t WHERE t.myTargetResourcePid = :id"); - q.setParameter("id", theId); - q.executeUpdate(); - - return null; - } - }); - } - - @Override - @Transactional(propagation = Propagation.NEVER) - public Integer performReindexingPass(final Integer theCount) { - if (getConfig().isStatusBasedReindexingDisabled()) { - return -1; - } - if (!myReindexLock.tryLock()) { - return -1; - } - try { - return doPerformReindexingPass(theCount); - } catch (ReindexFailureException e) { - ourLog.warn("Reindexing failed for resource {}", e.getResourceId()); - markResourceAsIndexingFailed(e.getResourceId()); - return -1; - } finally { - myReindexLock.unlock(); - } - } - - private class ResourceReindexingTask implements Runnable { - private final Long myNextId; - - public ResourceReindexingTask(Long theNextId) { - myNextId = theNextId; - } - - @SuppressWarnings("unchecked") - @Override - public void run() { - TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); - txTemplate.afterPropertiesSet(); - - Throwable reindexFailure; - try { - reindexFailure = txTemplate.execute(new TransactionCallback() { - @Override - public Throwable doInTransaction(TransactionStatus theStatus) { - ResourceTable resourceTable = myResourceTableDao.findById(myNextId).orElseThrow(IllegalStateException::new); - - try { - /* - * This part is because from HAPI 1.5 - 1.6 we changed the format of forced ID to be "type/id" instead of just "id" - */ - ForcedId forcedId = resourceTable.getForcedId(); - if (forcedId != null) { - if (isBlank(forcedId.getResourceType())) { - ourLog.info("Updating resource {} forcedId type to {}", forcedId.getForcedId(), resourceTable.getResourceType()); - forcedId.setResourceType(resourceTable.getResourceType()); - myForcedIdDao.save(forcedId); - } - } - - final IBaseResource resource = toResource(resourceTable, false); - - Class resourceClass = getContext().getResourceDefinition(resourceTable.getResourceType()).getImplementingClass(); - @SuppressWarnings("rawtypes") final IFhirResourceDao dao = getDaoOrThrowException(resourceClass); - dao.reindex(resource, resourceTable); - return null; - - } catch (Exception e) { - ourLog.error("Failed to index resource {}: {}", resourceTable.getIdDt(), e.toString(), e); - theStatus.setRollbackOnly(); - return e; - } - } - }); - } catch (ResourceVersionConflictException e) { - /* - * We reindex in multiple threads, so it's technically possible that two threads try - * to index resources that cause a constraint error now (i.e. because a unique index has been - * added that didn't previously exist). In this case, one of the threads would succeed and - * 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()); - reindexFailure = null; - } - - if (reindexFailure != null) { - txTemplate.execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus theStatus) { - ourLog.info("Setting resource PID[{}] status to ERRORED", myNextId); - myResourceTableDao.updateStatusToErrored(myNextId); - } - }); - } - } - } } 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 0bd421cc87c..022c4e3419d 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 @@ -1,7 +1,9 @@ package ca.uhn.fhir.jpa.dao; -import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; import ca.uhn.fhir.jpa.search.warm.WarmCacheEntry; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.util.JpaConstants; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import com.google.common.collect.Sets; @@ -36,21 +38,6 @@ import java.util.*; public class DaoConfig { - /** - * Default {@link #getTreatReferencesAsLogical() logical URL bases}. Includes the following - * values: - *

    - *
  • "http://hl7.org/fhir/valueset-*"
  • - *
  • "http://hl7.org/fhir/codesystem-*"
  • - *
  • "http://hl7.org/fhir/StructureDefinition/*"
  • - *
- */ - public static final Set DEFAULT_LOGICAL_BASE_URLS = Collections.unmodifiableSet(new HashSet(Arrays.asList( - "http://hl7.org/fhir/ValueSet/*", - "http://hl7.org/fhir/CodeSystem/*", - "http://hl7.org/fhir/valueset-*", - "http://hl7.org/fhir/codesystem-*", - "http://hl7.org/fhir/StructureDefinition/*"))); /** * Default value for {@link #setReuseCachedSearchResultsForMillis(Long)}: 60000ms (one minute) */ @@ -86,24 +73,22 @@ public class DaoConfig { ))); private static final Logger ourLog = LoggerFactory.getLogger(DaoConfig.class); private IndexEnabledEnum myIndexMissingFieldsEnabled = IndexEnabledEnum.DISABLED; + + /** + * Child Configurations + */ + + private ModelConfig myModelConfig = new ModelConfig(); + /** * update setter javadoc if default changes */ private Long myTranslationCachesExpireAfterWriteInMinutes = DEFAULT_TRANSLATION_CACHES_EXPIRE_AFTER_WRITE_IN_MINUTES; - /** - * update setter javadoc if default changes - */ - private boolean myAllowExternalReferences = false; - /** - * update setter javadoc if default changes - */ - private boolean myAllowContainsSearches = false; /** * update setter javadoc if default changes */ private boolean myAllowInlineMatchUrlReferences = true; private boolean myAllowMultipleDelete; - private boolean myDefaultSearchParamsCanBeOverridden = false; /** * update setter javadoc if default changes */ @@ -141,8 +126,6 @@ public class DaoConfig { private Long myReuseCachedSearchResultsForMillis = DEFAULT_REUSE_CACHED_SEARCH_RESULTS_FOR_MILLIS; private boolean mySchedulingDisabled; private boolean mySuppressUpdatesWithNoChange = true; - private Set myTreatBaseUrlsAsLocal = new HashSet<>(); - private Set myTreatReferencesAsLogical = new HashSet<>(DEFAULT_LOGICAL_BASE_URLS); private boolean myAutoCreatePlaceholderReferenceTargets; private Integer myCacheControlNoStoreMaxResultsUpperLimit = 1000; private Integer myCountSearchResultsUpTo = null; @@ -156,6 +139,8 @@ 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; /** * Constructor @@ -222,12 +207,7 @@ public class DaoConfig { * @see #setTreatReferencesAsLogical(Set) */ public void addTreatReferencesAsLogical(String theTreatReferencesAsLogical) { - validateTreatBaseUrlsAsLocal(theTreatReferencesAsLogical); - - if (myTreatReferencesAsLogical == null) { - myTreatReferencesAsLogical = new HashSet<>(); - } - myTreatReferencesAsLogical.add(theTreatReferencesAsLogical); + myModelConfig.addTreatReferencesAsLogical(theTreatReferencesAsLogical); } /** @@ -642,9 +622,39 @@ public class DaoConfig { myResourceMetaCountHardLimit = theResourceMetaCountHardLimit; } + /** + * Controls the behaviour when a client-assigned ID is encountered, i.e. an HTTP PUT + * on a resource ID that does not already exist in the database. + *

+ * Default is {@link ClientIdStrategyEnum#ALPHANUMERIC} + *

+ */ + public ClientIdStrategyEnum getResourceClientIdStrategy() { + return myResourceClientIdStrategy; + } + + /** + * Controls the behaviour when a client-assigned ID is encountered, i.e. an HTTP PUT + * on a resource ID that does not already exist in the database. + *

+ * Default is {@link ClientIdStrategyEnum#ALPHANUMERIC} + *

+ * + * @param theResourceClientIdStrategy Must not be null + */ + public void setResourceClientIdStrategy(ClientIdStrategyEnum theResourceClientIdStrategy) { + Validate.notNull(theResourceClientIdStrategy, "theClientIdStrategy must not be null"); + myResourceClientIdStrategy = theResourceClientIdStrategy; + } + /** * This setting configures the strategy to use in generating IDs for newly * created resources on the server. The default is {@link IdStrategyEnum#SEQUENTIAL_NUMERIC}. + *

+ * This strategy is only used for server-assigned IDs, i.e. for HTTP POST + * where the client is requesing that the server store a new resource and give + * it an ID. + *

*/ public IdStrategyEnum getResourceServerIdStrategy() { return myResourceServerIdStrategy; @@ -653,8 +663,13 @@ public class DaoConfig { /** * This setting configures the strategy to use in generating IDs for newly * created resources on the server. The default is {@link IdStrategyEnum#SEQUENTIAL_NUMERIC}. + *

+ * This strategy is only used for server-assigned IDs, i.e. for HTTP POST + * where the client is requesing that the server store a new resource and give + * it an ID. + *

* - * @param theResourceIdStrategy The strategy. Must not be null. + * @param theResourceIdStrategy The strategy. Must not be null. */ public void setResourceServerIdStrategy(IdStrategyEnum theResourceIdStrategy) { Validate.notNull(theResourceIdStrategy, "theResourceIdStrategy must not be null"); @@ -717,57 +732,6 @@ public class DaoConfig { myTranslationCachesExpireAfterWriteInMinutes = translationCachesExpireAfterWriteInMinutes; } - /** - * This setting may be used to advise the server that any references found in - * resources that have any of the base URLs given here will be replaced with - * simple local references. - *

- * For example, if the set contains the value http://example.com/base/ - * and a resource is submitted to the server that contains a reference to - * http://example.com/base/Patient/1, the server will automatically - * convert this reference to Patient/1 - *

- *

- * Note that this property has different behaviour from {@link DaoConfig#getTreatReferencesAsLogical()} - *

- * - * @see #getTreatReferencesAsLogical() - */ - public Set getTreatBaseUrlsAsLocal() { - return myTreatBaseUrlsAsLocal; - } - - /** - * This setting may be used to advise the server that any references found in - * resources that have any of the base URLs given here will be replaced with - * simple local references. - *

- * For example, if the set contains the value http://example.com/base/ - * and a resource is submitted to the server that contains a reference to - * http://example.com/base/Patient/1, the server will automatically - * convert this reference to Patient/1 - *

- * - * @param theTreatBaseUrlsAsLocal The set of base URLs. May be null, which - * means no references will be treated as external - */ - public void setTreatBaseUrlsAsLocal(Set theTreatBaseUrlsAsLocal) { - if (theTreatBaseUrlsAsLocal != null) { - for (String next : theTreatBaseUrlsAsLocal) { - validateTreatBaseUrlsAsLocal(next); - } - } - - HashSet treatBaseUrlsAsLocal = new HashSet(); - for (String next : ObjectUtils.defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet())) { - while (next.endsWith("/")) { - next = next.substring(0, next.length() - 1); - } - treatBaseUrlsAsLocal.add(next); - } - myTreatBaseUrlsAsLocal = treatBaseUrlsAsLocal; - } - /** * This setting may be used to advise the server that any references found in * resources that have any of the base URLs given here will be treated as logical @@ -787,10 +751,10 @@ public class DaoConfig { *
  • http://example.com/some-base* (will match anything beginning with the part before the *)
  • * * - * @see #DEFAULT_LOGICAL_BASE_URLS Default values for this property + * @see ModelConfig#DEFAULT_LOGICAL_BASE_URLS Default values for this property */ public Set getTreatReferencesAsLogical() { - return myTreatReferencesAsLogical; + return myModelConfig.getTreatReferencesAsLogical(); } /** @@ -812,49 +776,13 @@ public class DaoConfig { *
  • http://example.com/some-base* (will match anything beginning with the part before the *)
  • * * - * @see #DEFAULT_LOGICAL_BASE_URLS Default values for this property + * @see ModelConfig#DEFAULT_LOGICAL_BASE_URLS Default values for this property */ public DaoConfig setTreatReferencesAsLogical(Set theTreatReferencesAsLogical) { - myTreatReferencesAsLogical = theTreatReferencesAsLogical; + myModelConfig.setTreatReferencesAsLogical(theTreatReferencesAsLogical); return this; } - /** - * If enabled, the server will support the use of :contains searches, - * which are helpful but can have adverse effects on performance. - *

    - * Default is false (Note that prior to HAPI FHIR - * 3.5.0 the default was true) - *

    - *

    - * Note: If you change this value after data already has - * already been stored in the database, you must for a reindexing - * of all data in the database or resources may not be - * searchable. - *

    - */ - public boolean isAllowContainsSearches() { - return myAllowContainsSearches; - } - - /** - * If enabled, the server will support the use of :contains searches, - * which are helpful but can have adverse effects on performance. - *

    - * Default is false (Note that prior to HAPI FHIR - * 3.5.0 the default was true) - *

    - *

    - * Note: If you change this value after data already has - * already been stored in the database, you must for a reindexing - * of all data in the database or resources may not be - * searchable. - *

    - */ - public void setAllowContainsSearches(boolean theAllowContainsSearches) { - this.myAllowContainsSearches = theAllowContainsSearches; - } - /** * If set to true (default is false) the server will allow * resources to have references to external servers. For example if this server is @@ -881,7 +809,7 @@ public class DaoConfig { * @see #setAllowExternalReferences(boolean) */ public boolean isAllowExternalReferences() { - return myAllowExternalReferences; + return myModelConfig.isAllowExternalReferences(); } /** @@ -910,7 +838,7 @@ public class DaoConfig { * @see #setAllowExternalReferences(boolean) */ public void setAllowExternalReferences(boolean theAllowExternalReferences) { - myAllowExternalReferences = theAllowExternalReferences; + myModelConfig.setAllowExternalReferences(theAllowExternalReferences); } /** @@ -987,38 +915,6 @@ public class DaoConfig { myAutoCreatePlaceholderReferenceTargets = theAutoCreatePlaceholderReferenceTargets; } - /** - * If set to {@code true} the default search params (i.e. the search parameters that are - * defined by the FHIR specification itself) may be overridden by uploading search - * parameters to the server with the same code as the built-in search parameter. - *

    - * This can be useful if you want to be able to disable or alter - * the behaviour of the default search parameters. - *

    - *

    - * The default value for this setting is {@code false} - *

    - */ - public boolean isDefaultSearchParamsCanBeOverridden() { - return myDefaultSearchParamsCanBeOverridden; - } - - /** - * If set to {@code true} the default search params (i.e. the search parameters that are - * defined by the FHIR specification itself) may be overridden by uploading search - * parameters to the server with the same code as the built-in search parameter. - *

    - * This can be useful if you want to be able to disable or alter - * the behaviour of the default search parameters. - *

    - *

    - * The default value for this setting is {@code false} - *

    - */ - public void setDefaultSearchParamsCanBeOverridden(boolean theDefaultSearchParamsCanBeOverridden) { - myDefaultSearchParamsCanBeOverridden = theDefaultSearchParamsCanBeOverridden; - } - /** * If set to false (default is true) resources will be permitted to be * deleted even if other resources currently contain references to them. @@ -1236,7 +1132,7 @@ public class DaoConfig { /** * If set to true (default is true), indexes will be - * created for search parameters marked as {@link JpaConstants#EXT_SP_UNIQUE}. + * created for search parameters marked as {@link SearchParamConstants#EXT_SP_UNIQUE}. * This is a HAPI FHIR specific extension which can be used to specify that no more than one * resource can exist which matches a given criteria, using a database constraint to * enforce this. @@ -1247,7 +1143,7 @@ public class DaoConfig { /** * If set to true (default is true), indexes will be - * created for search parameters marked as {@link JpaConstants#EXT_SP_UNIQUE}. + * created for search parameters marked as {@link SearchParamConstants#EXT_SP_UNIQUE}. * This is a HAPI FHIR specific extension which can be used to specify that no more than one * resource can exist which matches a given criteria, using a database constraint to * enforce this. @@ -1353,18 +1249,8 @@ public class DaoConfig { * given number. *

    */ - public void setSearchPreFetchThresholds(List thePreFetchThresholds) { - Validate.isTrue(thePreFetchThresholds.size() > 0, "thePreFetchThresholds must not be empty"); - int last = 0; - for (Integer nextInteger : thePreFetchThresholds) { - int nextInt = nextInteger.intValue(); - Validate.isTrue(nextInt > 0 || nextInt == -1, nextInt + " is not a valid prefetch threshold"); - Validate.isTrue(nextInt != last, "Prefetch thresholds must be sequential"); - Validate.isTrue(nextInt > last || nextInt == -1, "Prefetch thresholds must be sequential"); - Validate.isTrue(last != -1, "Prefetch thresholds must be sequential"); - last = nextInt; - } - mySearchPreFetchThresholds = thePreFetchThresholds; + public List getSearchPreFetchThresholds() { + return mySearchPreFetchThresholds; } /** @@ -1380,8 +1266,18 @@ public class DaoConfig { * given number. *

    */ - public List getSearchPreFetchThresholds() { - return mySearchPreFetchThresholds; + public void setSearchPreFetchThresholds(List thePreFetchThresholds) { + Validate.isTrue(thePreFetchThresholds.size() > 0, "thePreFetchThresholds must not be empty"); + int last = 0; + for (Integer nextInteger : thePreFetchThresholds) { + int nextInt = nextInteger.intValue(); + Validate.isTrue(nextInt > 0 || nextInt == -1, nextInt + " is not a valid prefetch threshold"); + Validate.isTrue(nextInt != last, "Prefetch thresholds must be sequential"); + Validate.isTrue(nextInt > last || nextInt == -1, "Prefetch thresholds must be sequential"); + Validate.isTrue(last != -1, "Prefetch thresholds must be sequential"); + last = nextInt; + } + mySearchPreFetchThresholds = thePreFetchThresholds; } /** @@ -1412,6 +1308,161 @@ 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 ModelConfig getModelConfig() { + return myModelConfig; + } + + /** + * If enabled, the server will support the use of :contains searches, + * which are helpful but can have adverse effects on performance. + *

    + * Default is false (Note that prior to HAPI FHIR + * 3.5.0 the default was true) + *

    + *

    + * Note: If you change this value after data already has + * already been stored in the database, you must for a reindexing + * of all data in the database or resources may not be + * searchable. + *

    + */ + public boolean isAllowContainsSearches() { + return this.myModelConfig.isAllowContainsSearches(); + } + + /** + * If enabled, the server will support the use of :contains searches, + * which are helpful but can have adverse effects on performance. + *

    + * Default is false (Note that prior to HAPI FHIR + * 3.5.0 the default was true) + *

    + *

    + * Note: If you change this value after data already has + * already been stored in the database, you must for a reindexing + * of all data in the database or resources may not be + * searchable. + *

    + */ + public void setAllowContainsSearches(boolean theAllowContainsSearches) { + this.myModelConfig.setAllowContainsSearches(theAllowContainsSearches); + } + + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be replaced with + * simple local references. + *

    + * For example, if the set contains the value http://example.com/base/ + * and a resource is submitted to the server that contains a reference to + * http://example.com/base/Patient/1, the server will automatically + * convert this reference to Patient/1 + *

    + *

    + * Note that this property has different behaviour from {@link DaoConfig#getTreatReferencesAsLogical()} + *

    + * + * @see #getTreatReferencesAsLogical() + */ + public Set getTreatBaseUrlsAsLocal() { + return myModelConfig.getTreatBaseUrlsAsLocal(); + } + + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be replaced with + * simple local references. + *

    + * For example, if the set contains the value http://example.com/base/ + * and a resource is submitted to the server that contains a reference to + * http://example.com/base/Patient/1, the server will automatically + * convert this reference to Patient/1 + *

    + * + * @param theTreatBaseUrlsAsLocal The set of base URLs. May be null, which + * means no references will be treated as external + */ + public void setTreatBaseUrlsAsLocal(Set theTreatBaseUrlsAsLocal) { + myModelConfig.setTreatBaseUrlsAsLocal(theTreatBaseUrlsAsLocal); + } + + /** + * If set to {@code true} the default search params (i.e. the search parameters that are + * defined by the FHIR specification itself) may be overridden by uploading search + * parameters to the server with the same code as the built-in search parameter. + *

    + * This can be useful if you want to be able to disable or alter + * the behaviour of the default search parameters. + *

    + *

    + * The default value for this setting is {@code false} + *

    + */ + public boolean isDefaultSearchParamsCanBeOverridden() { + return myModelConfig.isDefaultSearchParamsCanBeOverridden(); + } + + /** + * If set to {@code true} the default search params (i.e. the search parameters that are + * defined by the FHIR specification itself) may be overridden by uploading search + * parameters to the server with the same code as the built-in search parameter. + *

    + * This can be useful if you want to be able to disable or alter + * the behaviour of the default search parameters. + *

    + *

    + * The default value for this setting is {@code false} + *

    + */ + public void setDefaultSearchParamsCanBeOverridden(boolean theDefaultSearchParamsCanBeOverridden) { + myModelConfig.setDefaultSearchParamsCanBeOverridden(theDefaultSearchParamsCanBeOverridden); + } + + public enum IndexEnabledEnum { ENABLED, DISABLED @@ -1429,16 +1480,33 @@ public class DaoConfig { UUID } - private static void validateTreatBaseUrlsAsLocal(String theUrl) { - Validate.notBlank(theUrl, "Base URL must not be null or empty"); + public enum ClientIdStrategyEnum { + /** + * Clients are not allowed to supply IDs for resources that do not + * already exist + */ + NOT_ALLOWED, - int starIdx = theUrl.indexOf('*'); - if (starIdx != -1) { - if (starIdx != theUrl.length() - 1) { - throw new IllegalArgumentException("Base URL wildcard character (*) can only appear at the end of the string: " + theUrl); - } - } + /** + * Clients may supply IDs but these IDs are not permitted to be purely + * numeric. In other words, values such as "A", "A1" and "000A" would be considered + * valid but "123" would not. + *

    This is the default setting.

    + */ + ALPHANUMERIC, + /** + * Clients may supply any ID including purely numeric IDs. Note that this setting should + * only be set on an empty database, or on a database that has always had this setting + * set as it causes a "forced ID" to be used for all resources. + *

    + * Note that if you use this setting, it is highly recommended that you also + * set the {@link #setResourceServerIdStrategy(IdStrategyEnum) ResourceServerIdStrategy} + * to {@link IdStrategyEnum#UUID} in order to avoid any potential for conflicts. Otherwise + * a database sequence will be used to generate IDs and these IDs can conflict with + * client-assigned numeric IDs. + *

    + */ + ANY } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoMethodOutcome.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoMethodOutcome.java index e9809f98873..5a2b398e52e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoMethodOutcome.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoMethodOutcome.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.dao; * #L% */ -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.rest.api.MethodOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; 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 144bbe63353..72795fd0afd 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 @@ -22,20 +22,29 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; +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; @@ -44,8 +53,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; @@ -53,25 +62,70 @@ public class DaoRegistry implements ApplicationContextAware { return retVal; } - public IFhirResourceDao getResourceDao(String theResourceName) { - IFhirResourceDao retVal = getResourceNameToResourceDao().get(theResourceName); - Validate.notNull(retVal, "No DAO exists for resource type %s - Have: %s", theResourceName, myResourceNameToResourceDao); - return retVal; - - } - - 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; + public IFhirResourceDao getResourceDao(String theResourceName) { + init(); + IFhirResourceDao retVal = myResourceNameToResourceDao.get(theResourceName); + if (retVal == null) { + List supportedResourceTypes = myResourceNameToResourceDao + .keySet() + .stream() + .sorted() + .collect(Collectors.toList()); + 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) { + 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 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); + } + + public IFhirResourceDao getSubscriptionDao() { + return getResourceDao(ResourceTypeEnum.SUBSCRIPTION.getCode()); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DatabaseSearchParamProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DatabaseSearchParamProvider.java new file mode 100644 index 00000000000..0c57157a14e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DatabaseSearchParamProvider.java @@ -0,0 +1,53 @@ +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.jpa.searchparam.registry.BaseSearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +public class DatabaseSearchParamProvider implements ISearchParamProvider { + @Autowired + private PlatformTransactionManager myTxManager; + @Autowired + private DaoRegistry myDaoRegistry; + + @Override + public IBundleProvider search(SearchParameterMap theParams) { + return myDaoRegistry.getResourceDao(ResourceTypeEnum.SEARCHPARAMETER.getCode()).search(theParams); + } + + @Override + public void refreshCache(BaseSearchParamRegistry theSearchParamRegistry, long theRefreshInterval) { + TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); + txTemplate.execute(t->{ + theSearchParamRegistry.doRefresh(theRefreshInterval); + return null; + }); + + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DeleteMethodOutcome.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DeleteMethodOutcome.java index e52ba24f41f..ce4656840e7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DeleteMethodOutcome.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DeleteMethodOutcome.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao; import java.util.List; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.rest.api.MethodOutcome; /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/EncodedResource.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/EncodedResource.java index b471ecb9106..41a1d52f261 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/EncodedResource.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/EncodedResource.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.dao; * #L% */ -import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; class EncodedResource { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2.java index 2b71fb6e7c2..0eae423fa12 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoDstu2.java @@ -11,7 +11,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.Include; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoEncounterDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoEncounterDstu2.java index 5dbccba51e6..51ab8e5132d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoEncounterDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoEncounterDstu2.java @@ -24,10 +24,11 @@ import java.util.Collections; import javax.servlet.http.HttpServletRequest; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu2.resource.Encounter; import ca.uhn.fhir.rest.api.SortSpec; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoPatientDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoPatientDstu2.java index 58cae84537b..4fe820c7f5c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoPatientDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoPatientDstu2.java @@ -24,10 +24,11 @@ import java.util.Collections; import javax.servlet.http.HttpServletRequest; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.rest.api.*; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java index a3ac54a7c45..31c163554ef 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoQuestionnaireResponseDstu2.java @@ -32,7 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; import ca.uhn.fhir.model.dstu2.resource.Questionnaire; import ca.uhn.fhir.model.dstu2.resource.QuestionnaireResponse; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java index 7c4e8bcb7bf..0e8756ca3a2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoSearchParameterR4; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.SearchParameter; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java index 72b44e1b251..7e48d0fb13a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSubscriptionDstu2.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.SubscriptionTable; import ca.uhn.fhir.model.dstu2.resource.Subscription; import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java index b3a87e35165..a0daeabc5a6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoValueSetDstu2.java @@ -27,6 +27,7 @@ import java.util.*; import javax.annotation.PostConstruct; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import org.apache.commons.codec.binary.StringUtils; import org.hl7.fhir.instance.hapi.validation.CachingValidationSupport; import org.hl7.fhir.instance.hapi.validation.DefaultProfileValidationSupport; @@ -37,7 +38,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.entity.BaseHasResource; +import ca.uhn.fhir.jpa.model.entity.BaseHasResource; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.model.dstu2.resource.ValueSet; 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 ff8899edf98..03578b02bd6 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 @@ -21,9 +21,10 @@ package ca.uhn.fhir.jpa.dao; */ import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.entity.TagDefinition; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; +import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; @@ -68,7 +69,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; @@ -81,6 +81,10 @@ 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) { ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size()); @@ -241,7 +245,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()); } @@ -363,7 +367,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { case POST: { // CREATE @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); + IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(res.getClass()); res.setId((String) null); DaoMethodOutcome outcome; outcome = resourceDao.create(res, nextReqEntry.getRequest().getIfNoneExist(), false, theRequestDetails); @@ -403,7 +407,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { case PUT: { // UPDATE @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); + IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(res.getClass()); DaoMethodOutcome outcome; 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 0e2f9a96e3b..a175c806c64 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 @@ -21,7 +21,9 @@ 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.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.param.StringParam; @@ -40,8 +42,7 @@ import org.hibernate.search.jpa.FullTextEntityManager; 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.hl7.fhir.instance.model.api.IAnyResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Transactional; @@ -65,6 +66,12 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { @Autowired protected IForcedIdDao myForcedIdDao; + @Autowired + private DaoConfig myDaoConfig; + + @Autowired + private IdHelperService myIdHelperService; + private Boolean ourDisabled; /** @@ -174,7 +181,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { addTextSearch(qb, bool, textAndTerms, "myNarrativeText", "myNarrativeTextEdgeNGram", "myNarrativeTextNGram"); if (theReferencingPid != null) { - bool.must(qb.keyword().onField("myResourceLinks.myTargetResourcePid").matching(theReferencingPid).createQuery()); + bool.must(qb.keyword().onField("myResourceLinksField").matching(theReferencingPid.toString()).createQuery()); } if (bool.isEmpty()) { @@ -194,13 +201,11 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { // execute search List result = jpaQuery.getResultList(); - HashSet pidsSet = pids != null ? new HashSet(pids) : null; - - ArrayList retVal = new ArrayList(); + ArrayList retVal = new ArrayList<>(); for (Object object : result) { Object[] nextArray = (Object[]) object; Long next = (Long) nextArray[0]; - if (next != null && (pidsSet == null || pidsSet.contains(next))) { + if (next != null) { retVal.add(next); } } @@ -212,9 +217,9 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { public List everything(String theResourceName, SearchParameterMap theParams) { Long pid = null; - if (theParams.get(BaseResource.SP_RES_ID) != null) { + if (theParams.get(IAnyResource.SP_RES_ID) != null) { String idParamValue; - IQueryParameterType idParam = theParams.get(BaseResource.SP_RES_ID).get(0).get(0); + IQueryParameterType idParam = theParams.get(IAnyResource.SP_RES_ID).get(0).get(0); if (idParam instanceof TokenParam) { TokenParam idParm = (TokenParam) idParam; idParamValue = idParm.getValue(); @@ -222,7 +227,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { StringParam idParm = (StringParam) idParam; idParamValue = idParm.getValue(); } - pid = BaseHapiFhirDao.translateForcedIdToPid(theResourceName, idParamValue, myForcedIdDao); + pid = myIdHelperService.translateForcedIdToPid(theResourceName, idParamValue); } Long referencingPid = pid; @@ -275,7 +280,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(contextParts[0], contextParts[1], myForcedIdDao); + Long pid = myIdHelperService.translateForcedIdToPid(contextParts[0], contextParts[1]); FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager); @@ -291,7 +296,8 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { .sentence(theText.toLowerCase()).createQuery(); Query query = qb.bool() - .must(qb.keyword().onField("myResourceLinks.myTargetResourcePid").matching(pid).createQuery()) +// .must(qb.keyword().onField("myResourceLinks.myTargetResourcePid").matching(pid).createQuery()) + .must(qb.keyword().onField("myResourceLinksField").matching(pid.toString()).createQuery()) .must(textQuery) .createQuery(); @@ -338,7 +344,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { } long delay = System.currentTimeMillis() - start; - ourLog.info("Provided {} suggestions for term {} in {} ms", new Object[]{terms.size(), theText, delay}); + ourLog.info("Provided {} suggestions for term {} in {} ms", terms.size(), theText, delay); return suggestions; } @@ -351,14 +357,14 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { private ArrayList myPartialMatchScores; private String myOriginalSearch; - public MySuggestionFormatter(String theOriginalSearch, List theSuggestions) { + MySuggestionFormatter(String theOriginalSearch, List theSuggestions) { myOriginalSearch = theOriginalSearch; mySuggestions = theSuggestions; } @Override public String highlightTerm(String theOriginalText, TokenGroup theTokenGroup) { - ourLog.debug("{} Found {} with score {}", new Object[]{myAnalyzer, theOriginalText, theTokenGroup.getTotalScore()}); + ourLog.debug("{} Found {} with score {}", myAnalyzer, theOriginalText, theTokenGroup.getTotalScore()); if (theTokenGroup.getTotalScore() > 0) { float score = theTokenGroup.getTotalScore(); if (theOriginalText.equalsIgnoreCase(myOriginalSearch)) { @@ -378,13 +384,13 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { return null; } - public void setAnalyzer(String theString) { + void setAnalyzer(String theString) { myAnalyzer = theString; } - public void setFindPhrasesWith() { - myPartialMatchPhrases = new ArrayList(); - myPartialMatchScores = new ArrayList(); + void setFindPhrasesWith() { + myPartialMatchPhrases = new ArrayList<>(); + myPartialMatchScores = new ArrayList<>(); for (Suggestion next : mySuggestions) { myPartialMatchPhrases.add(' ' + next.myTerm); @@ -401,7 +407,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { private String myTerm; private float myScore; - public Suggestion(String theTerm, float theScore) { + Suggestion(String theTerm, float theScore) { myTerm = theTerm; myScore = theScore; } 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 fd47335fb25..4a1867febbb 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,18 +1,14 @@ package ca.uhn.fhir.jpa.dao; -import java.util.Collection; -import java.util.Set; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.entity.BaseHasResource; +import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceTag; +import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import org.hl7.fhir.instance.model.api.IBaseResource; -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 java.util.Collection; /* * #%L @@ -42,10 +38,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 */ @@ -53,12 +45,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/IFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDao.java index d0eb43b7dd2..96e92dab98a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDao.java @@ -21,9 +21,10 @@ package ca.uhn.fhir.jpa.dao; */ import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.entity.BaseHasResource; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.model.entity.BaseHasResource; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.jpa.util.ExpungeOptions; import ca.uhn.fhir.jpa.util.ExpungeOutcome; @@ -170,6 +171,12 @@ public interface IFhirResourceDao extends IDao { */ T read(IIdType theId, RequestDetails theRequestDetails); + /** + * Should deleted resources be returned successfully. This should be false for + * a normal FHIR read. + */ + T read(IIdType theId, RequestDetails theRequestDetails, boolean theDeletedOk); + BaseHasResource readEntity(IIdType theId); /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirSystemDao.java index 8d75646bb01..d003f14d5ba 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirSystemDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirSystemDao.java @@ -53,13 +53,6 @@ public interface IFhirSystemDao extends IDao { IBundleProvider history(Date theDate, Date theUntil, RequestDetails theRequestDetails); - /** - * Marks all indexes as needing fresh indexing - * - * @return Returns the number of affected rows - */ - int markAllResourcesForReindexing(); - /** * Not supported for DSTU1 * @@ -67,8 +60,6 @@ public interface IFhirSystemDao extends IDao { */ MT metaGetOperation(RequestDetails theRequestDetails); - Integer performReindexingPass(Integer theCount); - T transaction(RequestDetails theRequestDetails, T theResources); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java index df6ca704bcb..933a6cd3c19 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.dao; import java.util.List; import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl.Suggestion; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; public interface IFulltextSearchSvc { 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 06281a3e022..3a9975259eb 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 @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.param.DateRangeParam; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -42,8 +43,8 @@ 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, - DateRangeParam theLastUpdated); + Set loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection theMatches, Set theRevIncludes, boolean theReverseMode, + DateRangeParam theLastUpdated, String theSearchIdOrDescription); /** * How many results may be fetched at once diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaValidationSupportDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaValidationSupportDstu2.java index 8078eed33b0..431332a7735 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaValidationSupportDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaValidationSupportDstu2.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.dstu2.resource.Questionnaire; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.TokenParam; 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 56d8809b00d..0aab8ba3a54 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 @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao; * 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,13 +21,21 @@ 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.ResourceIndexedSearchParams; -import ca.uhn.fhir.jpa.entity.*; -import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; +import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService; +import ca.uhn.fhir.jpa.entity.ResourceSearchView; +import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.util.StringNormalizer; +import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.jpa.term.VersionIndependentConcept; import ca.uhn.fhir.jpa.util.BaseIterator; @@ -69,10 +77,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 +100,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 +112,44 @@ 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 MatchResourceUrlService myMatchResourceUrlService; + @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 +157,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 +241,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 = myMatchResourceUrlService.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 +394,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 +406,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 +438,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); } @@ -441,6 +462,11 @@ public class SearchBuilder implements ISearchBuilder { } else if (def instanceof RuntimeChildResourceDefinition) { RuntimeChildResourceDefinition resDef = (RuntimeChildResourceDefinition) def; resourceTypes.addAll(resDef.getResourceTypes()); + if (resourceTypes.size() == 1) { + if (resourceTypes.get(0).isInterface()) { + throw new InvalidRequestException("Unable to perform search for unqualified chain '" + theParamName + "' as this SearchParameter does not declare any target types. Add a qualifier of the form '" + theParamName + ":[ResourceType]' to perform this search."); + } + } } else { throw new ConfigurationException("Property " + paramPath + " of type " + myResourceName + " is not a resource: " + def.getClass()); } @@ -458,10 +484,14 @@ public class SearchBuilder implements ISearchBuilder { resourceId = ref.getValue(); } else { - RuntimeResourceDefinition resDef = myContext.getResourceDefinition(ref.getResourceType()); - resourceTypes = new ArrayList<>(1); - resourceTypes.add(resDef.getImplementingClass()); - resourceId = ref.getIdPart(); + try { + RuntimeResourceDefinition resDef = myContext.getResourceDefinition(ref.getResourceType()); + resourceTypes = new ArrayList<>(1); + resourceTypes.add(resDef.getImplementingClass()); + resourceId = ref.getIdPart(); + } catch (DataFormatException e) { + throw new InvalidRequestException("Invalid resource type: " + ref.getResourceType()); + } } boolean foundChainMatch = false; @@ -491,10 +521,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 +542,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 +1192,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 +1202,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"); } } @@ -1190,8 +1219,8 @@ public class SearchBuilder implements ISearchBuilder { } if (myDontUseHashesForSearch) { - String likeExpression = BaseHapiFhirDao.normalizeString(rawSearchTerm); - if (myCallingDao.getConfig().isAllowContainsSearches()) { + String likeExpression = StringNormalizer.normalizeString(rawSearchTerm); + if (myDaoConfig.isAllowContainsSearches()) { if (theParameter instanceof StringParam) { if (((StringParam) theParameter).isContains()) { likeExpression = createLeftAndRightMatchLikeExpression(likeExpression); @@ -1226,17 +1255,17 @@ public class SearchBuilder implements ISearchBuilder { // Normalized Match - String normalizedString = BaseHapiFhirDao.normalizeString(rawSearchTerm); + String normalizedString = StringNormalizer.normalizeString(rawSearchTerm); 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.getModelConfig(), 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 +1502,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 +1629,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(myResourceName, idParm.getValue(), myForcedIdDao); + Long pid = myIdHelperService.translateForcedIdToPid(myResourceName, idParm.getValue()); if (myAlsoIncludePids == null) { myAlsoIncludePids = new ArrayList<>(1); } @@ -1677,7 +1706,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 +1742,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 +1836,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,8 +1986,8 @@ 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, - boolean theReverseMode, DateRangeParam theLastUpdated) { + 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 +2046,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; } @@ -2080,7 +2109,7 @@ public class SearchBuilder implements ISearchBuilder { nextRoundMatches = pidsToInclude; } while (includes.size() > 0 && nextRoundMatches.size() > 0 && addedSomeThisRound); - ourLog.info("Loaded {} {} in {} rounds and {} ms", allAdded.size(), theReverseMode ? "_revincludes" : "_includes", roundCounts, w.getMillisAndRestart()); + ourLog.info("Loaded {} {} in {} rounds and {} ms for search {}", allAdded.size(), theReverseMode ? "_revincludes" : "_includes", roundCounts, w.getMillisAndRestart(), theSearchIdOrDescription); return allAdded; } @@ -2088,6 +2117,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 +2129,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 +2287,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 +2315,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()); + Set newPids = loadIncludes(myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated(), mySearchUuid); myCurrentIterator = newPids.iterator(); } @@ -2368,7 +2367,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 +2482,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 +2620,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/TransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java index 554a3e58723..7b5f2400f77 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java @@ -22,8 +22,9 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; +import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.parser.DataFormatException; @@ -83,17 +84,10 @@ public class TransactionProcessor { private FhirContext myContext; @Autowired private ITransactionProcessorVersionAdapter myVersionAdapter; - - public static boolean isPlaceholder(IIdType theId) { - if (theId != null && theId.getValue() != null) { - return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:"); - } - return false; - } - - private static String toStatusString(int theStatusCode) { - return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); - } + @Autowired + private MatchUrlService myMatchUrlService; + @Autowired + private DaoRegistry myDaoRegistry; private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) { myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry); @@ -162,7 +156,6 @@ public class TransactionProcessor { return defaultString(theId.getValue()).startsWith(URN_PREFIX); } - public void setDao(BaseHapiFhirDao theDao) { myDao = theDao; } @@ -186,6 +179,40 @@ public class TransactionProcessor { } } + public BUNDLE collection(final RequestDetails theRequestDetails, BUNDLE theRequest) { + String transactionType = myVersionAdapter.getBundleType(theRequest); + + if (!org.hl7.fhir.r4.model.Bundle.BundleType.COLLECTION.toCode().equals(transactionType)) { + throw new InvalidRequestException("Can not process collection Bundle of type: " + transactionType); + } + + ourLog.info("Beginning storing collection with {} resources", myVersionAdapter.getEntries(theRequest).size()); + long start = System.currentTimeMillis(); + + TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode()); + + List resources = new ArrayList<>(); + for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) { + IBaseResource resource = myVersionAdapter.getResource(nextRequestEntry); + resources.add(resource); + } + + BUNDLE transactionBundle = myVersionAdapter.createBundle("transaction"); + for (IBaseResource next : resources) { + BUNDLEENTRY entry = myVersionAdapter.addEntry(transactionBundle); + myVersionAdapter.setResource(entry, next); + myVersionAdapter.setRequestVerb(entry, "PUT"); + myVersionAdapter.setRequestUrl(entry, next.getIdElement().toUnqualifiedVersionless().getValue()); + } + + transaction(theRequestDetails, transactionBundle); + + return resp; + } + private BUNDLE batch(final RequestDetails theRequestDetails, BUNDLE theRequest) { ourLog.info("Beginning batch with {} resources", myVersionAdapter.getEntries(theRequest).size()); long start = System.currentTimeMillis(); @@ -253,6 +280,7 @@ public class TransactionProcessor { validateDependencies(); String transactionType = myVersionAdapter.getBundleType(theRequest); + if (org.hl7.fhir.r4.model.Bundle.BundleType.BATCH.toCode().equals(transactionType)) { return batch(theRequestDetails, theRequest); } @@ -368,7 +396,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()); } @@ -459,284 +487,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) { @@ -749,7 +790,7 @@ public class TransactionProcessor { } private IFhirResourceDao getDaoOrThrowException(Class theClass) { - return myDao.getDaoOrThrowException(theClass); + return myDaoRegistry.getResourceDao(theClass); } protected void flushJpaSession() { @@ -844,18 +885,10 @@ public class TransactionProcessor { String getEntryRequestIfNoneMatch(BUNDLEENTRY theEntry); void setResponseOutcome(BUNDLEENTRY theEntry, IBaseOperationOutcome theOperationOutcome); - } - private static class BaseServerResponseExceptionHolder { - private BaseServerResponseException myException; + void setRequestVerb(BUNDLEENTRY theEntry, String theVerb); - public BaseServerResponseException getException() { - return myException; - } - - public void setException(BaseServerResponseException myException) { - this.myException = myException; - } + void setRequestUrl(BUNDLEENTRY theEntry, String theUrl); } /** @@ -961,4 +994,27 @@ public class TransactionProcessor { } + private static class BaseServerResponseExceptionHolder { + private BaseServerResponseException myException; + + public BaseServerResponseException getException() { + return myException; + } + + public void setException(BaseServerResponseException myException) { + this.myException = myException; + } + } + + public static boolean isPlaceholder(IIdType theId) { + if (theId != null && theId.getValue() != null) { + return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:"); + } + return false; + } + + private static String toStatusString(int theStatusCode) { + return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java index 9e2dc7c63e0..53132e9fd02 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IForcedIdDao.java @@ -27,7 +27,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import ca.uhn.fhir.jpa.entity.ForcedId; +import ca.uhn.fhir.jpa.model.entity.ForcedId; public interface IForcedIdDao extends JpaRepository { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java index 03fa39d7957..2a853945745 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTableDao.java @@ -8,11 +8,12 @@ import javax.persistence.TemporalType; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Temporal; import org.springframework.data.repository.query.Param; -import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; /* * #%L @@ -91,4 +92,8 @@ public interface IResourceHistoryTableDao extends JpaRepository findByResourceIds(@Param("pids") Collection pids); + + @Modifying + @Query("UPDATE ResourceHistoryTable r SET r.myResourceVersion = :newVersion WHERE r.myResourceId = :id AND r.myResourceVersion = :oldVersion") + void updateVersion(@Param("id") long theId, @Param("oldVersion") long theOldVersion, @Param("newVersion") long theNewVersion); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTagDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTagDao.java index 8d67e054898..408bd2504d8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTagDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceHistoryTagDao.java @@ -1,6 +1,6 @@ package ca.uhn.fhir.jpa.dao.data; -import ca.uhn.fhir.jpa.entity.ResourceHistoryTag; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTag; import org.springframework.data.jpa.repository.JpaRepository; /* diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java index 5e47044ef35..691db66f7af 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.dao.data; * #L% */ -import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamCoordsDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamCoordsDao.java index d0141d4d023..a5e13a1b5c6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamCoordsDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamCoordsDao.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao.data; import org.springframework.data.jpa.repository.JpaRepository; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamCoords; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; public interface IResourceIndexedSearchParamCoordsDao extends JpaRepository { // nothing yet diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamDateDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamDateDao.java index ec2ec3fdfab..4c06c12b1ac 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamDateDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamDateDao.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao.data; import org.springframework.data.jpa.repository.JpaRepository; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; public interface IResourceIndexedSearchParamDateDao extends JpaRepository { // nothing yet diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamNumberDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamNumberDao.java index deee2e7f367..dcbae493257 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamNumberDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamNumberDao.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao.data; import org.springframework.data.jpa.repository.JpaRepository; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamNumber; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; public interface IResourceIndexedSearchParamNumberDao extends JpaRepository { // nothing yet diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamQuantityDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamQuantityDao.java index 1a39d4c8f55..ddbe4ef41fb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamQuantityDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamQuantityDao.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao.data; import org.springframework.data.jpa.repository.JpaRepository; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; public interface IResourceIndexedSearchParamQuantityDao extends JpaRepository { // nothing yet diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamStringDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamStringDao.java index 7b1ae15b3e7..c5dad683c0d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamStringDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamStringDao.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao.data; import org.springframework.data.jpa.repository.JpaRepository; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamTokenDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamTokenDao.java index 9e30fd3b026..b8f21f299c0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamTokenDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamTokenDao.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.dao.data; * #L% */ -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamUriDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamUriDao.java index adc3e2e0fd6..51730816b28 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamUriDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamUriDao.java @@ -26,7 +26,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; public interface IResourceIndexedSearchParamUriDao extends JpaRepository { 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..296b0e18332 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 @@ -22,8 +22,10 @@ package ca.uhn.fhir.jpa.dao.data; import org.springframework.data.jpa.repository.JpaRepository; -import ca.uhn.fhir.jpa.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.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 new file mode 100644 index 00000000000..66091249dd4 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceReindexJobDao.java @@ -0,0 +1,67 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +/* + * #%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 interface IResourceReindexJobDao extends JpaRepository { + + @Modifying + @Query("UPDATE ResourceReindexJobEntity j SET j.myDeleted = true WHERE j.myResourceType = :type") + void markAllOfTypeAsDeleted(@Param("type") String theType); + + @Modifying + @Query("UPDATE ResourceReindexJobEntity j SET j.myDeleted = true") + void markAllOfTypeAsDeleted(); + + @Modifying + @Query("UPDATE ResourceReindexJobEntity j SET j.myDeleted = true WHERE j.myId = :pid") + void markAsDeletedById(@Param("pid") Long theId); + + @Query("SELECT j FROM ResourceReindexJobEntity j WHERE j.myDeleted = :deleted") + List findAll(Pageable thePage, @Param("deleted") boolean theDeleted); + + @Modifying + @Query("UPDATE ResourceReindexJobEntity j SET j.mySuspendedUntil = :suspendedUntil") + void setSuspendedUntil(@Param("suspendedUntil") Date theSuspendedUntil); + + @Modifying + @Query("UPDATE ResourceReindexJobEntity j SET j.myThresholdLow = :low WHERE j.myId = :id") + void setThresholdLow(@Param("id") Long theId, @Param("low") Date theLow); + + @Query("SELECT j.myReindexCount FROM ResourceReindexJobEntity j WHERE j.myId = :id") + Optional 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/data/IResourceTableDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java index 3737cdcbb5d..6a764f3df29 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java @@ -1,7 +1,6 @@ package ca.uhn.fhir.jpa.dao.data; -import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,6 +8,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.Date; import java.util.List; import java.util.Map; @@ -43,17 +43,19 @@ public interface IResourceTableDao extends JpaRepository { @Query("SELECT t.myId FROM ResourceTable t WHERE t.myId = :resid AND t.myResourceType = :restype AND t.myDeleted IS NOT NULL") Slice findIdsOfDeletedResourcesOfType(Pageable thePageable, @Param("resid") Long theResourceId, @Param("restype") String theResourceName); - @Query("SELECT t.myId FROM ResourceTable t WHERE t.myIndexStatus IS NULL") - Slice findIdsOfResourcesRequiringReindexing(Pageable thePageable); - - @Query("SELECT t.myResourceType as type, COUNT(*) as count FROM ResourceTable t GROUP BY t.myResourceType") + @Query("SELECT t.myResourceType as type, COUNT(t.myResourceType) as count FROM ResourceTable t GROUP BY t.myResourceType") List> getResourceCounts(); - @Modifying - @Query("UPDATE ResourceTable r SET r.myIndexStatus = null WHERE r.myResourceType = :restype") - int markResourcesOfTypeAsRequiringReindexing(@Param("restype") String theResourceType); + @Query("SELECT t.myId FROM ResourceTable t WHERE t.myUpdated >= :low AND t.myUpdated <= :high ORDER BY t.myUpdated DESC") + Slice findIdsOfResourcesWithinUpdatedRangeOrderedFromNewest(Pageable thePage, @Param("low") Date theLow, @Param("high") Date theHigh); + + @Query("SELECT t.myId FROM ResourceTable t WHERE t.myUpdated >= :low AND t.myUpdated <= :high ORDER BY t.myUpdated ASC") + Slice findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(Pageable thePage, @Param("low") Date theLow, @Param("high") Date theHigh); + + @Query("SELECT t.myId FROM ResourceTable t WHERE t.myUpdated >= :low AND t.myUpdated <= :high AND t.myResourceType = :restype ORDER BY t.myUpdated ASC") + Slice findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(Pageable thePage, @Param("restype") String theResourceType, @Param("low") Date theLow, @Param("high") Date theHigh); @Modifying - @Query("UPDATE ResourceTable r SET r.myIndexStatus = " + BaseHapiFhirDao.INDEX_STATUS_INDEXING_FAILED + " WHERE r.myId = :resid") - void updateStatusToErrored(@Param("resid") Long theId); + @Query("UPDATE ResourceTable t SET t.myIndexStatus = :status WHERE t.myId = :id") + void updateIndexStatus(@Param("id") Long theId, @Param("status") Long theIndexStatus); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTagDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTagDao.java index 1ba407c1f4f..5fdde232365 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTagDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTagDao.java @@ -26,7 +26,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import ca.uhn.fhir.jpa.entity.ResourceTag; +import ca.uhn.fhir.jpa.model.entity.ResourceTag; public interface IResourceTagDao extends JpaRepository { @Query("" + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamPresentDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamPresentDao.java index 3304826276c..d6ceda5125a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamPresentDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamPresentDao.java @@ -27,8 +27,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.entity.SearchParamPresent; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; public interface ISearchParamPresentDao extends JpaRepository { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchResultDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchResultDao.java index 448ff17d3cd..fd9be624720 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchResultDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchResultDao.java @@ -45,6 +45,10 @@ public interface ISearchResultDao extends JpaRepository { @Query(value="SELECT r.myId FROM SearchResult r WHERE r.mySearchPid = :search") Slice findForSearch(Pageable thePage, @Param("search") Long theSearchPid); + @Modifying + @Query("DELETE FROM SearchResult s WHERE s.myResourcePid IN :ids") + void deleteByResourceIds(@Param("ids") List theContent); + @Modifying @Query("DELETE FROM SearchResult s WHERE s.myId IN :ids") void deleteByIds(@Param("ids") List theContent); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISubscriptionTableDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISubscriptionTableDao.java index a60b9766201..53fe6c4971b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISubscriptionTableDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISubscriptionTableDao.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.dao.data; * #L% */ -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.SubscriptionTable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITagDefinitionDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITagDefinitionDao.java index 12a50fe94f8..527aea105af 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITagDefinitionDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITagDefinitionDao.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao.data; import org.springframework.data.jpa.repository.JpaRepository; -import ca.uhn.fhir.jpa.entity.TagDefinition; +import ca.uhn.fhir.jpa.model.entity.TagDefinition; public interface ITagDefinitionDao extends JpaRepository { // nothing 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..e38bf571824 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 @@ -21,15 +21,14 @@ package ca.uhn.fhir.jpa.dao.dstu3; */ import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; 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 implements IFhirResourceDaoSearchParameter { - @Autowired - private ISearchParamRegistry mySearchParamRegistry; - - @Autowired - private IFhirSystemDao mySystemDao; - protected void markAffectedResources(SearchParameter theResource) { Boolean reindex = theResource != null ? CURRENTLY_REINDEXING.get(theResource) : null; String expression = theResource != null ? theResource.getExpression() : null; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java index a161ae6dd3c..59af37689bc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSubscriptionDstu3.java @@ -24,7 +24,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSubscription; import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.SubscriptionTable; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.api.EncodingEnum; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java index 32e664faeb0..d6cad009fcb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; import ca.uhn.fhir.jpa.dao.TransactionProcessor; -import ca.uhn.fhir.jpa.entity.TagDefinition; +import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/JpaValidationSupportDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/JpaValidationSupportDstu3.java index d25090ca73f..80caa0c8656 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/JpaValidationSupportDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/JpaValidationSupportDstu3.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.UriParam; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java index ddb198db705..0a98816c6c0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java @@ -150,4 +150,14 @@ public class TransactionProcessorVersionAdapterDstu3 implements TransactionProce theEntry.getResponse().setOutcome((Resource) theOperationOutcome); } + @Override + public void setRequestVerb(Bundle.BundleEntryComponent theEntry, String theVerb) { + theEntry.getRequest().setMethod(Bundle.HTTPVerb.fromCode(theVerb)); + } + + @Override + public void setRequestUrl(Bundle.BundleEntryComponent theEntry, String theUrl) { + theEntry.getRequest().setUrl(theUrl); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseResourceLinkResolver.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseResourceLinkResolver.java new file mode 100644 index 00000000000..fe920db14bc --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseResourceLinkResolver.java @@ -0,0 +1,109 @@ +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.FhirContext; +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.DaoRegistry; +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +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.stereotype.Service; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; + +@Service +public class DatabaseResourceLinkResolver implements IResourceLinkResolver { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DatabaseResourceLinkResolver.class); + + @Autowired + private DaoConfig myDaoConfig; + @Autowired + private FhirContext myContext; + @Autowired + private IdHelperService myIdHelperService; + @Autowired + private DaoRegistry myDaoRegistry; + + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + protected EntityManager myEntityManager; + + @Override + public ResourceTable findTargetResource(RuntimeSearchParam theNextSpDef, String theNextPathsUnsplit, IIdType theNextId, String theTypeString, Class theType, String theId) { + ResourceTable target; + Long valueOf; + try { + valueOf = myIdHelperService.translateForcedIdToPid(theTypeString, theId); + } catch (ResourceNotFoundException e) { + if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) { + return null; + } + RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(theType); + String resName = missingResourceDef.getName(); + + if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) { + IBaseResource newResource = missingResourceDef.newInstance(); + newResource.setId(resName + "/" + theId); + 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 + "/" + theId + " not found, specified in path: " + theNextPathsUnsplit); + } + } + target = myEntityManager.find(ResourceTable.class, valueOf); + RuntimeResourceDefinition targetResourceDef = myContext.getResourceDefinition(theType); + if (target == null) { + String resName = targetResourceDef.getName(); + throw new InvalidRequestException("Resource " + resName + "/" + theId + " not found, specified in path: " + theNextPathsUnsplit); + } + + if (!theTypeString.equals(target.getResourceType())) { + throw new UnprocessableEntityException( + "Resource contains reference to " + theNextId.getValue() + " but resource with ID " + theNextId.getIdPart() + " is actually of type " + target.getResourceType()); + } + + if (target.getDeleted() != null) { + String resName = targetResourceDef.getName(); + throw new InvalidRequestException("Resource " + resName + "/" + theId + " is deleted, specified in path: " + theNextPathsUnsplit); + } + + if (theNextSpDef.getTargets() != null && !theNextSpDef.getTargets().contains(theTypeString)) { + return null; + } + return target; + } + + @Override + public void validateTypeOrThrowException(Class theType) { + myDaoRegistry.getDaoOrThrowException(theType); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseSearchParamSynchronizer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseSearchParamSynchronizer.java new file mode 100644 index 00000000000..27fe5d219f7 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseSearchParamSynchronizer.java @@ -0,0 +1,117 @@ +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.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; +import org.apache.commons.lang3.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Service +public class DatabaseSearchParamSynchronizer { + @Autowired + private DaoConfig myDaoConfig; + + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + protected EntityManager myEntityManager; + + public void synchronizeSearchParamsToDatabase(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) { + + synchronize(theParams, theEntity, theParams.stringParams, existingParams.stringParams); + synchronize(theParams, theEntity, theParams.tokenParams, existingParams.tokenParams); + synchronize(theParams, theEntity, theParams.numberParams, existingParams.numberParams); + synchronize(theParams, theEntity, theParams.quantityParams, existingParams.quantityParams); + synchronize(theParams, theEntity, theParams.dateParams, existingParams.dateParams); + synchronize(theParams, theEntity, theParams.uriParams, existingParams.uriParams); + synchronize(theParams, theEntity, theParams.coordsParams, existingParams.coordsParams); + synchronize(theParams, theEntity, theParams.links, existingParams.links); + + // make sure links are indexed + theEntity.setResourceLinks(theParams.links); + } + + private void synchronize(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Collection theNewParms, Collection theExistingParms) { + theParams.calculateHashes(theNewParms); + List quantitiesToRemove = subtract(theExistingParms, theNewParms); + List quantitiesToAdd = subtract(theNewParms, theExistingParms); + tryToReuseIndexEntities(quantitiesToRemove, quantitiesToAdd); + for (T next : quantitiesToRemove) { + myEntityManager.remove(next); + theEntity.getParamsQuantity().remove(next); + } + for (T next : quantitiesToAdd) { + myEntityManager.merge(next); + } + } + + /** + * The logic here is that often times when we update a resource we are dropping + * one index row and adding another. This method tries to reuse rows that would otherwise + * have been deleted by updating them with the contents of rows that would have + * otherwise been added. In other words, we're trying to replace + * "one delete + one insert" with "one update" + * + * @param theIndexesToRemove The rows that would be removed + * @param theIndexesToAdd The rows that would be added + */ + private void tryToReuseIndexEntities(List theIndexesToRemove, List theIndexesToAdd) { + for (int addIndex = 0; addIndex < theIndexesToAdd.size(); addIndex++) { + + // If there are no more rows to remove, there's nothing we can reuse + if (theIndexesToRemove.isEmpty()) { + break; + } + + T targetEntity = theIndexesToAdd.get(addIndex); + if (targetEntity.getId() != null) { + continue; + } + + // Take a row we were going to remove, and repurpose its ID + T entityToReuse = theIndexesToRemove.remove(theIndexesToRemove.size() - 1); + targetEntity.setId(entityToReuse.getId()); + } + } + + + + + List subtract(Collection theSubtractFrom, Collection theToSubtract) { + assert theSubtractFrom != theToSubtract; + + if (theSubtractFrom.isEmpty()) { + return new ArrayList<>(); + } + + ArrayList retVal = new ArrayList<>(theSubtractFrom); + retVal.removeAll(theToSubtract); + return retVal; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java new file mode 100644 index 00000000000..dbdf1a3bfe4 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -0,0 +1,103 @@ +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.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; +import ca.uhn.fhir.jpa.model.entity.ForcedId; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IIdType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Service +public class IdHelperService { + @Autowired + protected IForcedIdDao myForcedIdDao; + @Autowired(required = true) + private DaoConfig myDaoConfig; + + public void delete(ForcedId forcedId) { + myForcedIdDao.delete(forcedId); + } + + public Long translateForcedIdToPid(String theResourceName, String theResourceId) { + return translateForcedIdToPids(myDaoConfig, new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0); + } + + public List 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 8441deb4a6b..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IndexingSupport.java +++ /dev/null @@ -1,33 +0,0 @@ -package ca.uhn.fhir.jpa.dao.index; - -import java.util.Map; -import java.util.Set; - -import javax.persistence.EntityManager; - -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 deleted file mode 100644 index 9f4b5baf97e..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/ResourceIndexedSearchParams.java +++ /dev/null @@ -1,852 +0,0 @@ -package ca.uhn.fhir.jpa.dao.index; - -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.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; - -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; - - stringParams = new ArrayList<>(); - 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) { - theEntity.setParamsString(stringParams); - theEntity.setParamsStringPopulated(stringParams.isEmpty() == false); - theEntity.setParamsToken(tokenParams); - theEntity.setParamsTokenPopulated(tokenParams.isEmpty() == false); - theEntity.setParamsNumber(numberParams); - theEntity.setParamsNumberPopulated(numberParams.isEmpty() == false); - theEntity.setParamsQuantity(quantityParams); - theEntity.setParamsQuantityPopulated(quantityParams.isEmpty() == false); - theEntity.setParamsDate(dateParams); - theEntity.setParamsDatePopulated(dateParams.isEmpty() == false); - theEntity.setParamsUri(uriParams); - theEntity.setParamsUriPopulated(uriParams.isEmpty() == false); - theEntity.setParamsCoords(coordsParams); - theEntity.setParamsCoordsPopulated(coordsParams.isEmpty() == false); - theEntity.setParamsCompositeStringUniquePresent(compositeStringUniques.isEmpty() == false); - theEntity.setResourceLinks(links); - 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; - } - - /** - * This method is used to create a set of all possible combinations of - * parameters across a set of search parameters. An example of why - * this is needed: - *

    - * Let's say we have a unique index on (Patient:gender AND Patient:name). - * Then we pass in SMITH, John with a gender of male. - *

    - *

    - * In this case, because the name parameter matches both first and last name, - * we now need two unique indexes: - *

      - *
    • Patient?gender=male&name=SMITH
    • - *
    • Patient?gender=male&name=JOHN
    • - *
    - *

    - *

    - * So this recursive algorithm calculates those - *

    - * - * @param theResourceType E.g. Patient - * @param thePartsChoices E.g. [[gender=male], [name=SMITH, name=JOHN]] - */ - public static Set extractCompositeStringUniquesValueChains(String - theResourceType, List> thePartsChoices) { - - for (List next : thePartsChoices) { - next.removeIf(StringUtils::isBlank); - if (next.isEmpty()) { - return Collections.emptySet(); - } - } - - if (thePartsChoices.isEmpty()) { - return Collections.emptySet(); - } - - thePartsChoices.sort((o1, o2) -> { - String str1 = null; - String str2 = null; - if (o1.size() > 0) { - str1 = o1.get(0); - } - if (o2.size() > 0) { - str2 = o2.get(0); - } - return compare(str1, str2); - }); - - List values = new ArrayList<>(); - Set queryStringsToPopulate = new HashSet<>(); - extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices, values, queryStringsToPopulate); - return queryStringsToPopulate; - } - - private static void extractCompositeStringUniquesValueChains(String - theResourceType, List> thePartsChoices, List theValues, Set theQueryStringsToPopulate) { - if (thePartsChoices.size() > 0) { - List nextList = thePartsChoices.get(0); - Collections.sort(nextList); - for (String nextChoice : nextList) { - theValues.add(nextChoice); - extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices.subList(1, thePartsChoices.size()), theValues, theQueryStringsToPopulate); - theValues.remove(theValues.size() - 1); - } - } else { - if (theValues.size() > 0) { - StringBuilder uniqueString = new StringBuilder(); - uniqueString.append(theResourceType); - - for (int i = 0; i < theValues.size(); i++) { - uniqueString.append(i == 0 ? "?" : "&"); - uniqueString.append(theValues.get(i)); - } - - theQueryStringsToPopulate.add(uniqueString.toString()); - } - } - } - - - /** - * @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) { - 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; - } - - -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java new file mode 100644 index 00000000000..b8ba6b4efb6 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java @@ -0,0 +1,279 @@ +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.FhirContext; +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.IDao; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao; +import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; +import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.model.api.IQueryParameterType; +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.util.FhirTerser; +import ca.uhn.fhir.util.UrlUtil; +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.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.isNotBlank; + +@Service +@Lazy +public class SearchParamWithInlineReferencesExtractor { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamWithInlineReferencesExtractor.class); + + @Autowired + private MatchResourceUrlService myMatchResourceUrlService; + @Autowired + private DaoConfig myDaoConfig; + @Autowired + private FhirContext myContext; + @Autowired + private IdHelperService myIdHelperService; + @Autowired + private ISearchParamRegistry mySearchParamRegistry; + @Autowired + SearchParamExtractorService mySearchParamExtractorService; + @Autowired + ResourceLinkExtractor myResourceLinkExtractor; + @Autowired + DatabaseResourceLinkResolver myDatabaseResourceLinkResolver; + @Autowired + DatabaseSearchParamSynchronizer myDatabaseSearchParamSynchronizer; + @Autowired + private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; + + + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + protected EntityManager myEntityManager; + + public void populateFromResource(ResourceIndexedSearchParams theParams, IDao theCallingDao, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams existingParams) { + mySearchParamExtractorService.extractFromResource(theParams, theEntity, theResource); + + Set> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet(); + if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) { + theParams.findMissingSearchParams(myDaoConfig.getModelConfig(), theEntity, activeSearchParams); + } + + theParams.setUpdatedTime(theUpdateTime); + + extractInlineReferences(theResource); + + myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDatabaseResourceLinkResolver); + + /* + * 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); + } + + private void extractCompositeStringUniques(ResourceTable theEntity, ResourceIndexedSearchParams theParams) { + + final 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)); + } + } + } + } + + + + /** + * 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 = myMatchResourceUrlService.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); + } + } + } + + public void storeCompositeStringUniques(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) { + + // Store composite string uniques + if (myDaoConfig.isUniqueIndexesEnabled()) { + for (ResourceIndexedCompositeStringUnique next : myDatabaseSearchParamSynchronizer.subtract(existingParams.compositeStringUniques, theParams.compositeStringUniques)) { + ourLog.debug("Removing unique index: {}", next); + myEntityManager.remove(next); + theEntity.getParamsCompositeStringUnique().remove(next); + } + for (ResourceIndexedCompositeStringUnique next : myDatabaseSearchParamSynchronizer.subtract(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); + } + } + } +} 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..4901f45cd0f 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 @@ -21,15 +21,14 @@ package ca.uhn.fhir.jpa.dao.r4; */ import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; 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/dao/r4/FhirResourceDaoCompositionR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCompositionR4.java index 6dbe159ca52..fa4cc63684e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCompositionR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCompositionR4.java @@ -21,8 +21,7 @@ package ca.uhn.fhir.jpa.dao.r4; */ import ca.uhn.fhir.jpa.dao.IFhirResourceDaoComposition; -import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -32,7 +31,6 @@ import ca.uhn.fhir.rest.param.StringParam; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Composition; -import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.http.HttpServletRequest; import java.util.Collections; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoConceptMapR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoConceptMapR4.java index 4a73bdc8fa7..e333027a39d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoConceptMapR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoConceptMapR4.java @@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.dao.r4; */ import ca.uhn.fhir.jpa.dao.IFhirResourceDaoConceptMap; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoEncounterR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoEncounterR4.java index c02415fb92c..6f62c59bf09 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoEncounterR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoEncounterR4.java @@ -29,8 +29,8 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoEncounter; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.server.IBundleProvider; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoPatientR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoPatientR4.java index f88706fac71..032b6951185 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoPatientR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoPatientR4.java @@ -24,14 +24,14 @@ import java.util.Collections; import javax.servlet.http.HttpServletRequest; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.CacheControlDirective; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.springframework.beans.factory.annotation.Autowired; import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.SortSpec; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4.java index a28750864e8..0903d82c023 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.MethodOutcome; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java index 7648d725649..e2a7c73ee62 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java @@ -2,18 +2,22 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter; import ca.uhn.fhir.jpa.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.ElementUtil; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; +import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.utils.FHIRLexer; +import org.hl7.fhir.r4.utils.FHIRPathEngine; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; @@ -42,6 +46,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4 implements IFhirResourceDaoSearchParameter { + public static final DefaultProfileValidationSupport VALIDATION_SUPPORT = new DefaultProfileValidationSupport(); @Autowired private IFhirSystemDao mySystemDao; @@ -88,7 +93,7 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4 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); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java index db5be3e65c0..37d7bd035a4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java @@ -23,12 +23,12 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.OperationOutcome; -import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; import java.util.Date; import java.util.List; @@ -150,4 +150,14 @@ public class TransactionProcessorVersionAdapterR4 implements TransactionProcesso theEntry.getResponse().setOutcome((Resource) theOperationOutcome); } + @Override + public void setRequestVerb(Bundle.BundleEntryComponent theEntry, String theVerb) { + theEntry.getRequest().setMethod(Bundle.HTTPVerb.fromCode(theVerb)); + } + + @Override + public void setRequestUrl(Bundle.BundleEntryComponent theEntry, String theUrl) { + theEntry.getRequest().setUrl(theUrl); + } + } 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 new file mode 100644 index 00000000000..93db38c84ed --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceReindexJobEntity.java @@ -0,0 +1,141 @@ +package ca.uhn.fhir.jpa.entity; + +/* + * #%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 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; +import java.util.Date; + +@Entity +@Table(name = "HFJ_RES_REINDEX_JOB") +public class ResourceReindexJobEntity implements Serializable { + @Id + @SequenceGenerator(name = "SEQ_RES_REINDEX_JOB", sequenceName = "SEQ_RES_REINDEX_JOB") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RES_REINDEX_JOB") + @Column(name = "PID") + private Long myId; + @Column(name = "RES_TYPE", nullable = true) + private String myResourceType; + /** + * Inclusive + */ + @Column(name = "UPDATE_THRESHOLD_HIGH", nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date myThresholdHigh; + @Column(name = "JOB_DELETED", nullable = false) + private boolean myDeleted; + /** + * Inclusive + */ + @Column(name = "UPDATE_THRESHOLD_LOW", nullable = true) + @Temporal(TemporalType.TIMESTAMP) + private Date myThresholdLow; + @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; + } + + public void setSuspendedUntil(Date theSuspendedUntil) { + mySuspendedUntil = theSuspendedUntil; + } + + /** + * Inclusive + */ + public Date getThresholdLow() { + return myThresholdLow; + } + + /** + * Inclusive + */ + public void setThresholdLow(Date theThresholdLow) { + myThresholdLow = theThresholdLow; + } + + public String getResourceType() { + return myResourceType; + } + + public void setResourceType(String theResourceType) { + myResourceType = theResourceType; + } + + /** + * Inclusive + */ + public Date getThresholdHigh() { + return myThresholdHigh; + } + + /** + * Inclusive + */ + public void setThresholdHigh(Date theThresholdHigh) { + myThresholdHigh = theThresholdHigh; + } + + public Long getId() { + return myId; + } + + @VisibleForTesting + public void setIdForUnitTest(long theId) { + myId = theId; + } + + 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/ResourceSearchView.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java index 22526db8030..441f5b52f4c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java @@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.entity; */ import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.api.Constants; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/Search.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/Search.java index f8737108eed..84ceed8d407 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/Search.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/Search.java @@ -1,6 +1,6 @@ package ca.uhn.fhir.jpa.entity; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.param.DateRangeParam; import org.apache.commons.lang3.SerializationUtils; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchResult.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchResult.java index 0041073cd24..2511714d27c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchResult.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchResult.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.entity; * #L% */ +import ca.uhn.fhir.jpa.model.entity.ResourceTable; + import java.io.Serializable; import javax.persistence.Column; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionTable.java index b1f79ef9302..931200fbf90 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionTable.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SubscriptionTable.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.entity; * #L% */ +import ca.uhn.fhir.jpa.model.entity.ResourceTable; + import javax.persistence.*; import java.util.Date; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystem.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystem.java index d7e0e5e4c8e..acb18d0afa0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystem.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystem.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java index f5d0e9c4b5f..75f2ca2adea 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermCodeSystemVersion.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.util.CoverageIgnore; import javax.persistence.*; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMap.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMap.java index 2589a6bd990..97bc23f6682 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMap.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptMap.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; 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..8531d481c5d 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 @@ -24,8 +24,8 @@ 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.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.BaseHasResource; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.model.entity.BaseHasResource; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; @@ -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/BaseJpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java index 89483e7ccb1..e603f726fb5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.provider; */ import ca.uhn.fhir.jpa.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.util.ExpungeOptions; import ca.uhn.fhir.jpa.util.ExpungeOutcome; import ca.uhn.fhir.rest.annotation.At; @@ -31,6 +32,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Parameters; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Required; import javax.servlet.http.HttpServletRequest; @@ -42,11 +44,17 @@ public class BaseJpaSystemProvider extends BaseJpaProvider { public static final String PERFORM_REINDEXING_PASS = "$perform-reindexing-pass"; private IFhirSystemDao myDao; + @Autowired + private IResourceReindexingSvc myResourceReindexingSvc; public BaseJpaSystemProvider() { // nothing } + protected IResourceReindexingSvc getResourceReindexingSvc() { + return myResourceReindexingSvc; + } + protected Parameters doExpunge(IPrimitiveType theLimit, IPrimitiveType theExpungeDeletedResources, IPrimitiveType theExpungeOldVersions, IPrimitiveType theExpungeEverything) { ExpungeOptions options = createExpungeOptions(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything); ExpungeOutcome outcome = getDao().expunge(options); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java index 2d0f3e92876..1b6eb10678c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java @@ -34,11 +34,11 @@ public abstract class BaseJpaSystemProviderDstu2Plus extends BaseJpaSyste @OperationParam(name = "status") }) public IBaseResource markAllResourcesForReindexing() { - Integer count = getDao().markAllResourcesForReindexing(); + getResourceReindexingSvc().markAllResourcesForReindexing(); IBaseParameters retVal = ParametersUtil.newInstance(getContext()); - IPrimitiveType string = ParametersUtil.createString(getContext(), "Marked " + count + " resources"); + IPrimitiveType string = ParametersUtil.createString(getContext(), "Marked resources"); ParametersUtil.addParameterToParameters(getContext(), retVal, "status", string); return retVal; @@ -48,7 +48,7 @@ public abstract class BaseJpaSystemProviderDstu2Plus extends BaseJpaSyste @OperationParam(name = "status") }) public IBaseResource performReindexingPass() { - Integer count = getDao().performReindexingPass(1000); + Integer count = getResourceReindexingSvc().runReindexingPass(); IBaseParameters retVal = ParametersUtil.newInstance(getContext()); 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..f206e7b064c 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,37 @@ 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.model.dstu2.valueset.ResourceTypeEnum; 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 +60,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; - } + return myFhirContext.getResourceDefinition(ResourceTypeEnum.SUBSCRIPTION.getCode()).getImplementingClass(); } } 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..c4926dbcc2c 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.searchparam.registry.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..501e1a9b69e 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.searchparam.registry.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/ISearchCoordinatorSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ISearchCoordinatorSvc.java index 760474dd88d..89eb40cba65 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ISearchCoordinatorSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/ISearchCoordinatorSvc.java @@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.search; */ import ca.uhn.fhir.jpa.dao.IDao; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.server.IBundleProvider; 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 27177cc5c05..2d8536a32de 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 @@ -24,8 +24,8 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.IDao; import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.data.ISearchDao; -import ca.uhn.fhir.jpa.entity.BaseHasResource; -import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.BaseHasResource; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.model.primitive.InstantDt; @@ -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())); - includedPids.addAll(sb.loadIncludes(myDao, myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated())); + 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/PersistedJpaSearchFirstPageBundleProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaSearchFirstPageBundleProvider.java index bc8b61a00c4..09338aed558 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaSearchFirstPageBundleProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaSearchFirstPageBundleProvider.java @@ -65,6 +65,15 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED); List retVal = txTemplate.execute(theStatus -> toResourceList(mySearchBuilder, pids)); + int totalCountWanted = theToIndex - theFromIndex; + if (retVal.size() < totalCountWanted) { + if (mySearch.getStatus() == SearchStatusEnum.PASSCMPLET) { + int remainingWanted = totalCountWanted - retVal.size(); + int fromIndex = theToIndex - remainingWanted; + List remaining = super.getResources(fromIndex, theToIndex); + retVal.addAll(remaining); + } + } ourLog.trace("Loaded resources to return"); return retVal; 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 b6eb3a89253..5881a030da9 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 @@ -26,6 +26,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 ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.Constants; @@ -47,11 +48,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 +73,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; @@ -168,7 +174,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { verifySearchHasntFailedOrThrowInternalErrorException(search); if (search.getStatus() == SearchStatusEnum.FINISHED) { - ourLog.info("Search entity marked as finished"); + ourLog.info("Search entity marked as finished with {} results", search.getNumFound()); break; } if (search.getNumFound() >= theTo) { @@ -189,7 +195,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { search = newSearch.get(); String resourceType = search.getResourceType(); SearchParameterMap params = search.getSearchParameterMap(); - SearchContinuationTask task = new SearchContinuationTask(search, myDaoRegistry.getResourceDao(resourceType), params, resourceType); + IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(resourceType); + SearchContinuationTask task = new SearchContinuationTask(search, resourceDao, params, resourceType); myIdToSearchTask.put(search.getUuid(), task); myExecutor.submit(task); } @@ -228,17 +235,18 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); txTemplate.afterPropertiesSet(); return txTemplate.execute(t -> { - Optional searchOpt = mySearchDao.findById(theSearch.getId()); - Search search = searchOpt.orElseThrow(IllegalStateException::new); + Search search = mySearchDao.findById(theSearch.getId()).orElse(theSearch); + if (search.getStatus() != SearchStatusEnum.PASSCMPLET) { - throw new IllegalStateException("Can't change to LOADING because state is " + search.getStatus()); + throw new IllegalStateException("Can't change to LOADING because state is " + theSearch.getStatus()); } - theSearch.setStatus(SearchStatusEnum.LOADING); - Search newSearch = mySearchDao.save(theSearch); + search.setStatus(SearchStatusEnum.LOADING); + Search newSearch = mySearchDao.save(search); return Optional.of(newSearch); }); } catch (Exception e) { ourLog.warn("Failed to activate search: {}", e.toString()); + ourLog.trace("Failed to activate search", e); return Optional.empty(); } } @@ -310,8 +318,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())); - includedPids.addAll(sb.loadIncludes(theCallingDao, myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated())); + 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); @@ -438,6 +446,11 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { myManagedTxManager = theTxManager; } + @VisibleForTesting + public void setDaoRegistryForUnitTest(DaoRegistry theDaoRegistry) { + myDaoRegistry = theDaoRegistry; + } + public abstract class BaseTask implements Callable { private final SearchParameterMap myParams; private final IDao myCallingDao; @@ -486,14 +499,38 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { } public List getResourcePids(int theFromIndex, int theToIndex) { - ourLog.info("Requesting search PIDs from {}-{}", theFromIndex, theToIndex); + ourLog.debug("Requesting search PIDs from {}-{}", theFromIndex, theToIndex); boolean keepWaiting; do { synchronized (mySyncedPids) { ourLog.trace("Search status is {}", mySearch.getStatus()); - keepWaiting = mySyncedPids.size() < theToIndex && mySearch.getStatus() == SearchStatusEnum.LOADING; + boolean haveEnoughResults = mySyncedPids.size() >= theToIndex; + if (!haveEnoughResults) { + switch (mySearch.getStatus()) { + case LOADING: + keepWaiting = true; + break; + case PASSCMPLET: + /* + * If we get here, it means that the user requested resources that crossed the + * current pre-fetch boundary. For example, if the prefetch threshold is 50 and the + * user has requested resources 0-60, then they would get 0-50 back but the search + * coordinator would then stop searching.SearchCoordinatorSvcImplTest + */ + keepWaiting = false; + break; + case FAILED: + case FINISHED: + default: + keepWaiting = false; + break; + } + } else { + keepWaiting = false; + } } + if (keepWaiting) { ourLog.info("Waiting, as we only have {} results", mySyncedPids.size()); try { @@ -808,6 +845,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { * Construct the SQL query we'll be sending to the database */ IResultIterator theResultIterator = sb.createQuery(myParams, mySearch.getUuid()); + assert (theResultIterator != null); /* * The following loop actually loads the PIDs of the resources @@ -869,6 +907,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { txTemplate.afterPropertiesSet(); txTemplate.execute(t -> { List previouslyAddedResourcePids = mySearchResultDao.findWithSearchUuid(getSearch()); + ourLog.debug("Have {} previously added IDs in search: {}", previouslyAddedResourcePids.size(), getSearch().getUuid()); setPreviouslyAddedResourcePids(previouslyAddedResourcePids); return null; }); 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..e16931d9a1d 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 @@ -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 new file mode 100644 index 00000000000..7963413b83c --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/IResourceReindexingSvc.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.jpa.search.reindex; + +/*- + * #%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 interface IResourceReindexingSvc { + + /** + * Marks all indexes as needing fresh indexing + * + * @return Returns the job ID + */ + Long markAllResourcesForReindexing(); + + /** + * Marks all indexes of the given type as needing fresh indexing + * + * @return Returns the job ID + */ + 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. + */ + Integer runReindexingPass(); + + /** + * Does the same thing as {@link #runReindexingPass()} but makes sure to perform at + * least one pass even if one is half finished + */ + int forceReindexingPass(); + + /** + * Cancels all running and future reindexing jobs. This is mainly intended + * to be used by unit tests. + */ + void cancelAndPurgeAllJobs(); +} 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 new file mode 100644 index 00000000000..362fa8675e3 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java @@ -0,0 +1,505 @@ +package ca.uhn.fhir.jpa.search.reindex; + +/*- + * #%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.BaseHapiFhirDao; +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.data.IForcedIdDao; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; +import ca.uhn.fhir.jpa.dao.data.IResourceReindexJobDao; +import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; +import ca.uhn.fhir.jpa.model.entity.ForcedId; +import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import ca.uhn.fhir.util.StopWatch; +import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.apache.commons.lang3.time.DateUtils; +import org.hibernate.search.util.impl.Executors; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.annotation.PostConstruct; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; +import javax.persistence.Query; +import javax.transaction.Transactional; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +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; + @Autowired + private DaoConfig myDaoConfig; + @Autowired + private PlatformTransactionManager myTxManager; + private TransactionTemplate myTxTemplate; + private ThreadFactory myReindexingThreadFactory = new BasicThreadFactory.Builder().namingPattern("ResourceReindex-%d").build(); + private ThreadPoolExecutor myTaskExecutor; + @Autowired + private IResourceTableDao myResourceTableDao; + @Autowired + private IResourceHistoryTableDao myResourceHistoryTableDao; + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired + private IForcedIdDao myForcedIdDao; + @Autowired + private FhirContext myContext; + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + private EntityManager myEntityManager; + + @VisibleForTesting + void setReindexJobDaoForUnitTest(IResourceReindexJobDao theReindexJobDao) { + myReindexJobDao = theReindexJobDao; + } + + @VisibleForTesting + void setDaoConfigForUnitTest(DaoConfig theDaoConfig) { + myDaoConfig = theDaoConfig; + } + + @VisibleForTesting + void setTxManagerForUnitTest(PlatformTransactionManager theTxManager) { + myTxManager = theTxManager; + } + + @VisibleForTesting + void setResourceTableDaoForUnitTest(IResourceTableDao theResourceTableDao) { + myResourceTableDao = theResourceTableDao; + } + + @VisibleForTesting + void setDaoRegistryForUnitTest(DaoRegistry theDaoRegistry) { + myDaoRegistry = theDaoRegistry; + } + + @VisibleForTesting + void setForcedIdDaoForUnitTest(IForcedIdDao theForcedIdDao) { + myForcedIdDao = theForcedIdDao; + } + + @VisibleForTesting + void setContextForUnitTest(FhirContext theContext) { + myContext = theContext; + } + + @PostConstruct + public void start() { + myTxTemplate = new TransactionTemplate(myTxManager); + initExecutor(); + } + + private void initExecutor() { + // Create the threadpool executor used for reindex jobs + int reindexThreadCount = myDaoConfig.getReindexThreadCount(); + RejectedExecutionHandler rejectHandler = new Executors.BlockPolicy(); + myTaskExecutor = new ThreadPoolExecutor(0, reindexThreadCount, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(100), + myReindexingThreadFactory, + rejectHandler + ); + } + + @Override + @Transactional(Transactional.TxType.REQUIRED) + public Long markAllResourcesForReindexing() { + return markAllResourcesForReindexing(null); + } + + @Override + @Transactional(Transactional.TxType.REQUIRED) + public Long markAllResourcesForReindexing(String theType) { + String typeDesc; + if (isNotBlank(theType)) { + myReindexJobDao.markAllOfTypeAsDeleted(theType); + typeDesc = theType; + } else { + myReindexJobDao.markAllOfTypeAsDeleted(); + typeDesc = "(any)"; + } + + ResourceReindexJobEntity job = new ResourceReindexJobEntity(); + job.setResourceType(theType); + job.setThresholdHigh(DateUtils.addMinutes(new Date(), 5)); + 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; + } + if (myIndexingLock.tryLock()) { + try { + return doReindexingPassInsideLock(); + } finally { + myIndexingLock.unlock(); + } + } + return null; + } + + private Integer doReindexingPassInsideLock() { + expungeJobsMarkedAsDeleted(); + return runReindexJobs(); + } + + @Override + public int forceReindexingPass() { + myIndexingLock.lock(); + try { + return doReindexingPassInsideLock(); + } finally { + myIndexingLock.unlock(); + } + } + + @Override + public void cancelAndPurgeAllJobs() { + ourLog.info("Cancelling and purging all resource reindexing jobs"); + myTxTemplate.execute(t -> { + myReindexJobDao.markAllOfTypeAsDeleted(); + return null; + }); + + myTaskExecutor.shutdown(); + initExecutor(); + + expungeJobsMarkedAsDeleted(); + } + + private int runReindexJobs() { + 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.getThresholdLow() != null && next.getThresholdLow().getTime() >= next.getThresholdHigh().getTime()) { + markJobAsDeleted(next); + continue; + } + + count += runReindexJob(next); + } + return count; + } + + private void markJobAsDeleted(ResourceReindexJobEntity theJob) { + ourLog.info("Marking reindexing job ID[{}] as deleted", theJob.getId()); + myTxTemplate.execute(t -> { + myReindexJobDao.markAsDeletedById(theJob.getId()); + return null; + }); + } + + private int runReindexJob(ResourceReindexJobEntity theJob) { + if (theJob.getSuspendedUntil() != null) { + if (theJob.getSuspendedUntil().getTime() > System.currentTimeMillis()) { + return 0; + } + } + + ourLog.info("Performing reindex pass for JOB[{}]", theJob.getId()); + StopWatch sw = new StopWatch(); + AtomicInteger counter = new AtomicInteger(); + + // Calculate range + Date low = theJob.getThresholdLow() != null ? theJob.getThresholdLow() : BEGINNING_OF_TIME; + Date high = theJob.getThresholdHigh(); + + // Query for resources within threshold + StopWatch pageSw = new StopWatch(); + Slice range = myTxTemplate.execute(t -> { + PageRequest page = PageRequest.of(0, PASS_SIZE); + if (isNotBlank(theJob.getResourceType())) { + return myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(page, theJob.getResourceType(), low, high); + } else { + return myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(page, low, high); + } + }); + 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 + .stream() + .map(t -> myTaskExecutor.submit(new ResourceReindexingTask(t, counter))) + .collect(Collectors.toList()); + + Date latestDate = null; + for (Future next : futures) { + Date nextDate; + try { + nextDate = next.get(); + } catch (Exception e) { + ourLog.error("Failure reindexing", e); + Date suspendedUntil = DateUtils.addMinutes(new Date(), 1); + myTxTemplate.execute(t -> { + myReindexJobDao.setSuspendedUntil(suspendedUntil); + return null; + }); + return counter.get(); + } + + if (nextDate != null) { + if (latestDate == null || latestDate.getTime() < nextDate.getTime()) { + latestDate = new Date(nextDate.getTime()); + } + } + } + + Validate.notNull(latestDate); + Date newLow; + if (latestDate.getTime() == low.getTime()) { + 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; + } + + 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), newLow); + return counter.get(); + } + + private void expungeJobsMarkedAsDeleted() { + myTxTemplate.execute(t -> { + Collection toDelete = myReindexJobDao.findAll(PageRequest.of(0, 10), true); + toDelete.forEach(job -> { + ourLog.info("Purging deleted job[{}]", job.getId()); + myReindexJobDao.deleteById(job.getId()); + }); + return null; + }); + } + + @SuppressWarnings("JpaQlInspection") + private void markResourceAsIndexingFailed(final long theId) { + TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + txTemplate.execute((TransactionCallback) theStatus -> { + ourLog.info("Marking resource with PID {} as indexing_failed", new Object[]{theId}); + + myResourceTableDao.updateIndexStatus(theId, BaseHapiFhirDao.INDEX_STATUS_INDEXING_FAILED); + + Query q = myEntityManager.createQuery("DELETE FROM ResourceTag t WHERE t.myResourceId = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamCoords t WHERE t.myResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamDate t WHERE t.myResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamNumber t WHERE t.myResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamQuantity t WHERE t.myResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamString t WHERE t.myResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamToken t WHERE t.myResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + q = myEntityManager.createQuery("DELETE FROM ResourceIndexedSearchParamUri t WHERE t.myResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + q = myEntityManager.createQuery("DELETE FROM ResourceLink t WHERE t.mySourceResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + q = myEntityManager.createQuery("DELETE FROM ResourceLink t WHERE t.myTargetResourcePid = :id"); + q.setParameter("id", theId); + q.executeUpdate(); + + return null; + }); + } + + private class ResourceReindexingTask implements Callable { + private final Long myNextId; + private final AtomicInteger myCounter; + private Date myUpdated; + + ResourceReindexingTask(Long theNextId, AtomicInteger theCounter) { + myNextId = theNextId; + myCounter = theCounter; + } + + + @SuppressWarnings("unchecked") + private void doReindex(ResourceTable theResourceTable, T theResource) { + RuntimeResourceDefinition resourceDefinition = myContext.getResourceDefinition(theResource.getClass()); + Class resourceClass = (Class) resourceDefinition.getImplementingClass(); + final IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceClass); + dao.reindex(theResource, theResourceTable); + + myCounter.incrementAndGet(); + } + + @Override + public Date call() { + Throwable reindexFailure; + try { + reindexFailure = myTxTemplate.execute(t -> { + ResourceTable resourceTable = myResourceTableDao.findById(myNextId).orElseThrow(IllegalStateException::new); + myUpdated = resourceTable.getUpdatedDate(); + + try { + /* + * This part is because from HAPI 1.5 - 1.6 we changed the format of forced ID to be "type/id" instead of just "id" + */ + ForcedId forcedId = resourceTable.getForcedId(); + if (forcedId != null) { + if (isBlank(forcedId.getResourceType())) { + ourLog.info("Updating resource {} forcedId type to {}", forcedId.getForcedId(), resourceTable.getResourceType()); + forcedId.setResourceType(resourceTable.getResourceType()); + myForcedIdDao.save(forcedId); + } + } + + IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceTable.getResourceType()); + long expectedVersion = resourceTable.getVersion(); + IBaseResource resource = dao.read(resourceTable.getIdDt().toVersionless(), null,true); + if (resource == null) { + throw new InternalErrorException("Could not find resource version " + resourceTable.getIdDt().toUnqualified().getValue() + " in database"); + } + + Long actualVersion = resource.getIdElement().getVersionIdPartAsLong(); + if (actualVersion < expectedVersion) { + ourLog.warn("Resource {} version {} does not exist, renumbering version {}", resource.getIdElement().toUnqualifiedVersionless().getValue(), resource.getIdElement().getVersionIdPart(), expectedVersion); + myResourceHistoryTableDao.updateVersion(resourceTable.getId(), actualVersion, expectedVersion); + } + + doReindex(resourceTable, resource); + return null; + + } catch (Exception e) { + ourLog.error("Failed to index resource {}: {}", resourceTable.getIdDt(), e.toString(), e); + t.setRollbackOnly(); + return e; + } + }); + + } catch (ResourceVersionConflictException e) { + /* + * We reindex in multiple threads, so it's technically possible that two threads try + * to index resources that cause a constraint error now (i.e. because a unique index has been + * added that didn't previously exist). In this case, one of the threads would succeed and + * 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()); + reindexFailure = null; + } + + if (reindexFailure != null) { + ourLog.info("Setting resource PID[{}] status to ERRORED", myNextId); + markResourceAsIndexingFailed(myNextId); + } + + return myUpdated; + } + } +} 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..17375a2b955 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.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.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/ISearchParamPresenceSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/ISearchParamPresenceSvc.java index 551aac0338a..13607cfa821 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/ISearchParamPresenceSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/ISearchParamPresenceSvc.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.sp; * #L% */ -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import java.util.Map; 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..9495b795081 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 @@ -22,13 +22,15 @@ package ca.uhn.fhir.jpa.sp; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.entity.SearchParamPresent; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.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..e1418316073 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,28 @@ 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.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.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +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.model.dstu2.valueset.ResourceTypeEnum; 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 +38,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 +60,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 +99,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 +109,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 +300,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 +329,6 @@ public abstract class BaseSubscriptionInterceptor exten myProcessingChannel = theProcessingChannel; } - protected IFhirResourceDao getSubscriptionDao() { - return mySubscriptionDao; - } - public List getRegisteredSubscriptions() { return new ArrayList<>(myIdToSubscription.values()); } @@ -378,7 +368,8 @@ public abstract class BaseSubscriptionInterceptor exten RequestDetails req = new ServletSubRequestDetails(); req.setSubRequest(true); - IBundleProvider subscriptionBundleList = getSubscriptionDao().search(map, req); + IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao(); + 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 +427,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 +451,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 +494,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 +501,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 +535,8 @@ public abstract class BaseSubscriptionInterceptor exten } if (mySubscriptionActivatingSubscriber == null) { - mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(getSubscriptionDao(), getChannelType(), this, myTxManager, myAsyncTaskExecutor); + IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao(); + mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(subscriptionDao, getChannelType(), this, myTxManager, myAsyncTaskExecutor); } registerSubscriptionCheckingSubscriber(); @@ -619,5 +600,31 @@ public abstract class BaseSubscriptionInterceptor exten return myIdToSubscription.remove(subscriptionId); } + public IFhirResourceDao getSubscriptionDao() { + return myDaoRegistry.getSubscriptionDao(); + } + 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..3a7d3fff7fc 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,41 @@ 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 ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; 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.getSubscriptionDao(); + } + public Subscription.SubscriptionChannelType getChannelType() { return myChannelType; } @@ -71,7 +87,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/SubscriptionActivatingSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java index 209f5cb23bc..44f488b9c54 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 @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.SubscriptionUtil; import com.google.common.annotations.VisibleForTesting; @@ -162,16 +163,16 @@ public class SubscriptionActivatingSubscriber { @SuppressWarnings("EnumSwitchStatementWhichMissesCases") public void handleMessage(ResourceModifiedMessage.OperationTypeEnum theOperationType, IIdType theId, final IBaseResource theSubscription) throws MessagingException { - + if (!theId.getResourceType().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { + 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..480759e0f34 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.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; +import ca.uhn.fhir.jpa.searchparam.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..96bd3f9c032 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java @@ -0,0 +1,466 @@ +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.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.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.model.dstu2.valueset.ResourceTypeEnum; +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.getSubscriptionDao(); + IIdType subscriptionId = theSubscriptionId; + if (subscriptionId.hasResourceType() == false) { + subscriptionId = subscriptionId.withResourceType(ResourceTypeEnum.SUBSCRIPTION.getCode()); + } + 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) { + // Restore interrupted state... + Thread.currentThread().interrupt(); + 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/ISubscriptionMatcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java~HEAD similarity index 100% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java~HEAD 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 ce1788c61c8..4614fde8207 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 @@ -1,36 +1,58 @@ 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.jpa.dao.DaoRegistry; +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; +import ca.uhn.fhir.jpa.searchparam.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(); @@ -41,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.getSubscriptionDao(); + 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 deleted file mode 100644 index 3355e415005..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java +++ /dev/null @@ -1,12 +0,0 @@ -package ca.uhn.fhir.jpa.subscription.matcher; - -import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; - -public class SubscriptionMatcherInMemory implements ISubscriptionMatcher { - - @Override - public boolean match(String criteria, ResourceModifiedMessage msg) { - // FIXME KHS implement - return true; - } -} 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..9d1ac1c4209 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 @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem; import ca.uhn.fhir.jpa.dao.data.*; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; import ca.uhn.fhir.jpa.util.ScrollableResultsIterator; @@ -65,6 +66,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -134,6 +136,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, private Cache> myTranslationWithReverseCache; private int myFetchSize = DEFAULT_FETCH_SIZE; private ApplicationContext myApplicationContext; + private TransactionTemplate myTxTemplate; /** * @param theAdd If true, add the code. If false, remove the code. @@ -365,6 +368,9 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, } } + @Autowired + private PlatformTransactionManager myTransactionManager; + @Override @Transactional public void deleteConceptMapAndChildren(ResourceTable theResourceTable) { @@ -383,7 +389,12 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, break; } - theDao.deleteInBatch(link); + TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + txTemplate.execute(t->{ + theDao.deleteInBatch(link); + return null; + }); count += link.getNumberOfElements(); ourLog.info(" * {} {} deleted - {}/sec - ETA: {}", count, theDescriptor, sw.formatThroughput(count, TimeUnit.SECONDS), sw.getEstimatedTimeRemaining(count, totalCount)); @@ -863,8 +874,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) { @@ -888,7 +899,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, } TransactionTemplate tt = new TransactionTemplate(myTransactionMgr); - tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); tt.execute(new TransactionCallbackWithoutResult() { private void createParentsString(StringBuilder theParentsBuilder, Long theConceptPid) { Validate.notNull(theConceptPid, "theConceptPid must not be null"); @@ -1015,7 +1026,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, } TransactionTemplate tt = new TransactionTemplate(myTransactionMgr); - tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); if (!myDeferredConcepts.isEmpty() || !myConceptLinksToSaveLater.isEmpty()) { tt.execute(t -> { processDeferredConcepts(); @@ -1051,6 +1062,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, @PostConstruct public void start() { myCodeSystemResourceDao = myApplicationContext.getBean(IFhirResourceDaoCodeSystem.class); + myTxTemplate = new TransactionTemplate(myTransactionManager); } @Override @@ -1064,8 +1076,6 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, // Grab the existing versions so we can delete them later List existing = myCodeSystemVersionDao.findByCodeSystemResource(theCodeSystemResourcePid); -// verifyNoDuplicates(theCodeSystemVersion.getConcepts(), new HashSet()); - /* * For now we always delete old versions.. At some point it would be nice to allow configuration to keep old versions */ 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/IHapiTerminologySvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/IHapiTerminologySvc.java index f4bca9c6000..60222afd596 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/IHapiTerminologySvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/IHapiTerminologySvc.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.term; import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; 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..23d8243b364 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 @@ -67,7 +67,7 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { public static final String LOINC_ANSWERLIST_LINK_FILE = "LoincAnswerListLink.csv"; public static final String LOINC_DOCUMENT_ONTOLOGY_FILE = "DocumentOntology.csv"; public static final String LOINC_UPLOAD_PROPERTIES_FILE = "loincupload.properties"; - public static final String LOINC_FILE = "Loinc.csv"; + public static final String LOINC_FILE = "LoincTable/Loinc.csv"; public static final String LOINC_HIERARCHY_FILE = "MultiAxialHierarchy.csv"; public static final String LOINC_PART_FILE = "Part.csv"; public static final String LOINC_PART_LINK_FILE = "LoincPartLink.csv"; @@ -135,6 +135,7 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { } else { matches = nextFilename.endsWith("/" + theFileNamePart) || nextFilename.equals(theFileNamePart); } + if (matches) { ourLog.info("Processing file {}", nextFilename); foundMatch = true; @@ -188,48 +189,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/JpaConstants.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java index 40e33fd26d5..f155a961edf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java @@ -24,8 +24,6 @@ import ca.uhn.fhir.rest.api.Constants; public class JpaConstants { - public static final String EXT_SP_UNIQUE = "http://hapifhir.io/fhir/StructureDefinition/sp-unique"; - /** *

    * This extension should be of type string and should be 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 index 1ed136f8791..abb4a03b87a 100644 --- 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 @@ -1,5 +1,3 @@ -package ca.uhn.fhir.jpa.util; - /*- * #%L * HAPI FHIR JPA Server @@ -19,101 +17,3 @@ package ca.uhn.fhir.jpa.util; * limitations under the License. * #L% */ - -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.IFhirSystemDao; -import org.apache.commons.lang3.time.DateUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -import java.util.concurrent.Semaphore; - -public class ReindexController implements IReindexController { - - private static final Logger ourLog = LoggerFactory.getLogger(ReindexController.class); - private final Semaphore myReindexingLock = new Semaphore(1); - @Autowired - private DaoConfig myDaoConfig; - @Autowired - private IFhirSystemDao mySystemDao; - private Long myDontReindexUntil; - - /** - * This method is called once per minute to perform any required re-indexing. - *

    - * If nothing if found that requires reindexing, the query will not fire again for - * a longer amount of time. - *

    - * During most passes this will just check and find that there are no resources - * requiring re-indexing. In that case the method just returns immediately. - * If the search finds that some resources require reindexing, the system will - * do a bunch of reindexing and then return. - */ - @Scheduled(fixedDelay = DateUtils.MILLIS_PER_MINUTE) - @Transactional(propagation = Propagation.NEVER) - @Override - public void performReindexingPass() { - if (myDaoConfig.isSchedulingDisabled() || myDaoConfig.isStatusBasedReindexingDisabled()) { - return; - } - - synchronized (this) { - if (myDontReindexUntil != null && myDontReindexUntil > System.currentTimeMillis()) { - return; - } - } - - if (!myReindexingLock.tryAcquire()) { - ourLog.trace("Not going to reindex in parallel threads"); - return; - } - Integer count; - try { - count = mySystemDao.performReindexingPass(100); - - for (int i = 0; i < 50 && count != null && count != 0; i++) { - count = mySystemDao.performReindexingPass(100); - try { - Thread.sleep(DateUtils.MILLIS_PER_SECOND); - } catch (InterruptedException e) { - break; - } - } - } catch (Exception e) { - ourLog.error("Failure during reindex", e); - count = -1; - } finally { - myReindexingLock.release(); - } - - synchronized (this) { - if (count == null) { - ourLog.info("Reindex pass complete, no remaining resource to index"); - myDontReindexUntil = System.currentTimeMillis() + DateUtils.MILLIS_PER_HOUR; - } else if (count == -1) { - // Reindexing failed - myDontReindexUntil = System.currentTimeMillis() + DateUtils.MILLIS_PER_HOUR; - } else { - ourLog.info("Reindex pass complete, {} remaining resource to index", count); - myDontReindexUntil = null; - } - } - - } - - /** - * Calling this will cause a reindex loop to be triggered sooner that it would otherwise - */ - @Override - public void requestReindex() { - synchronized (this) { - myDontReindexUntil = null; - } - } - - -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java index dc3e746cefb..772709eb2c8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.model.dstu2.resource.Subscription; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import ca.uhn.fhir.model.dstu2.valueset.SubscriptionChannelTypeEnum; import ca.uhn.fhir.model.dstu2.valueset.SubscriptionStatusEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; @@ -47,14 +48,14 @@ public class SubscriptionsRequireManualActivationInterceptorDstu2 extends Server @Override public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { - if (myDao.getContext().getResourceDefinition(theResource).getName().equals("Subscription")) { + if (myDao.getContext().getResourceDefinition(theResource).getName().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.CREATE, null, theResource); } } @Override public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { - if (myDao.getContext().getResourceDefinition(theNewResource).getName().equals("Subscription")) { + if (myDao.getContext().getResourceDefinition(theNewResource).getName().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.UPDATE, theOldResource, theNewResource); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java index d8263c72626..48001d24b46 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; @@ -47,14 +48,14 @@ public class SubscriptionsRequireManualActivationInterceptorDstu3 extends Server @Override public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { - if (myDao.getContext().getResourceDefinition(theResource).getName().equals("Subscription")) { + if (myDao.getContext().getResourceDefinition(theResource).getName().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.CREATE, null, theResource); } } @Override public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { - if (myDao.getContext().getResourceDefinition(theNewResource).getName().equals("Subscription")) { + if (myDao.getContext().getResourceDefinition(theNewResource).getName().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.UPDATE, theOldResource, theNewResource); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java index fdccf19f0e4..fef6b658c53 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; @@ -47,14 +48,14 @@ public class SubscriptionsRequireManualActivationInterceptorR4 extends ServerOpe @Override public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { - if (myDao.getContext().getResourceDefinition(theResource).getName().equals("Subscription")) { + if (myDao.getContext().getResourceDefinition(theResource).getName().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.CREATE, null, theResource); } } @Override public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { - if (myDao.getContext().getResourceDefinition(theNewResource).getName().equals("Subscription")) { + if (myDao.getContext().getResourceDefinition(theNewResource).getName().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.UPDATE, theOldResource, theNewResource); } } 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/config/IdentifierLengthTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/IdentifierLengthTest.java index 2c97da80840..d66467491db 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/IdentifierLengthTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/IdentifierLengthTest.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.config; import org.junit.Test; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.util.TestUtil; public class IdentifierLengthTest { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java index 21edfb9c3d5..422c7ecf663 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu2Config.java @@ -11,6 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; import org.springframework.core.env.Environment; import org.springframework.orm.jpa.JpaTransactionManager; @@ -27,6 +28,7 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.*; @Configuration +@Import(TestJPAConfig.class) @EnableTransactionManagement() public class TestDstu2Config extends BaseJavaConfigDstu2 { private static final Logger ourLog = LoggerFactory.getLogger(TestDstu2Config.class); @@ -44,12 +46,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { private Exception myLastStackTrace; private String myLastStackTraceThreadName; - @Bean() - public DaoConfig daoConfig() { - return new DaoConfig(); - } - - @Bean() + @Bean public DataSource dataSource() { BasicDataSource retVal = new BasicDataSource() { @@ -118,7 +115,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu2"); @@ -159,16 +156,5 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { return requestValidator; } - @Bean() - public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { - JpaTransactionManager retVal = new JpaTransactionManager(); - retVal.setEntityManagerFactory(entityManagerFactory); - return retVal; - } - - @Bean - public UnregisterScheduledProcessor unregisterScheduledProcessor(Environment theEnv) { - return new UnregisterScheduledProcessor(theEnv); - } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java index eb692759bcd..545962d4246 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java @@ -9,10 +9,7 @@ import ca.uhn.fhir.validation.ResultSeverityEnum; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.apache.commons.dbcp2.BasicDataSource; import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; -import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.*; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.env.Environment; import org.springframework.orm.jpa.JpaTransactionManager; @@ -28,13 +25,14 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.*; @Configuration +@Import(TestJPAConfig.class) @EnableTransactionManagement() public class TestDstu3Config extends BaseJavaConfigDstu3 { static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestDstu3Config.class); private Exception myLastStackTrace; - @Bean() + @Bean public BasicDataSource basicDataSource() { BasicDataSource retVal = new BasicDataSource() { @@ -99,12 +97,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { return retVal; } - @Bean() - public DaoConfig daoConfig() { - return new DaoConfig(); - } - - @Bean() + @Bean @Primary() public DataSource dataSource() { @@ -127,12 +120,11 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu3"); retVal.setDataSource(dataSource()); - retVal.setPersistenceProvider(new HibernatePersistenceProvider()); retVal.setJpaProperties(jpaProperties()); return retVal; } @@ -166,18 +158,6 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { return requestValidator; } - @Bean() - public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { - JpaTransactionManager retVal = new JpaTransactionManager(); - retVal.setEntityManagerFactory(entityManagerFactory); - return retVal; - } - - @Bean - public UnregisterScheduledProcessor unregisterScheduledProcessor(Environment theEnv) { - return new UnregisterScheduledProcessor(theEnv); - } - /** * This lets the "@Value" fields reference properties from the properties file */ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3WithoutLuceneConfig.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3WithoutLuceneConfig.java index a6df9f278be..d3f66cb21d8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3WithoutLuceneConfig.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3WithoutLuceneConfig.java @@ -25,7 +25,7 @@ public class TestDstu3WithoutLuceneConfig extends TestDstu3Config { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setJpaProperties(jpaProperties()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java new file mode 100644 index 00000000000..dfceb79fc87 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.jpa.config; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.orm.jpa.JpaTransactionManager; + +import javax.persistence.EntityManagerFactory; + +@Configuration +public class TestJPAConfig { + + @Bean + public DaoConfig daoConfig() { + DaoConfig daoConfig = new DaoConfig(); + return daoConfig; + } + + @Bean + public ModelConfig modelConfig() { + return daoConfig().getModelConfig(); + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager retVal = new JpaTransactionManager(); + retVal.setEntityManagerFactory(entityManagerFactory); + return retVal; + } + + @Bean + public UnregisterScheduledProcessor unregisterScheduledProcessor(Environment theEnv) { + return new UnregisterScheduledProcessor(theEnv); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index 1d86d6e2070..d92b393293d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -4,12 +4,12 @@ import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.ResultSeverityEnum; import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; -import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder; import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.apache.commons.dbcp2.BasicDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; import org.springframework.core.env.Environment; import org.springframework.orm.jpa.JpaTransactionManager; @@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.fail; @Configuration +@Import(TestJPAConfig.class) @EnableTransactionManagement() public class TestR4Config extends BaseJavaConfigR4 { @@ -44,12 +45,7 @@ public class TestR4Config extends BaseJavaConfigR4 { private Exception myLastStackTrace; - @Bean() - public DaoConfig daoConfig() { - return new DaoConfig(); - } - - @Bean() + @Bean public DataSource dataSource() { BasicDataSource retVal = new BasicDataSource() { @@ -121,7 +117,7 @@ public class TestR4Config extends BaseJavaConfigR4 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("PU_HapiFhirJpaR4"); @@ -159,18 +155,6 @@ public class TestR4Config extends BaseJavaConfigR4 { return requestValidator; } - @Bean() - public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { - JpaTransactionManager retVal = new JpaTransactionManager(); - retVal.setEntityManagerFactory(entityManagerFactory); - return retVal; - } - - @Bean - public UnregisterScheduledProcessor unregisterScheduledProcessor(Environment theEnv) { - return new UnregisterScheduledProcessor(theEnv); - } - public static int getMaxThreads() { return ourMaxThreads; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4WithoutLuceneConfig.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4WithoutLuceneConfig.java index e77663e301e..e21d15fce13 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4WithoutLuceneConfig.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4WithoutLuceneConfig.java @@ -25,7 +25,7 @@ public class TestR4WithoutLuceneConfig extends TestR4Config { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setDataSource(dataSource()); 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..9415641cca3 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,52 @@ 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.jpa.model.util.StringNormalizer; +import ca.uhn.fhir.jpa.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +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()); @@ -49,14 +59,6 @@ public class BaseHapiFhirDaoTest extends BaseJpaTest { observation.setEffective(period); } - - @Test - public void testNormalizeString() { - assertEquals("TEST TEST", BaseHapiFhirDao.normalizeString("TEST teSt")); - assertEquals("AEIØU", BaseHapiFhirDao.normalizeString("åéîøü")); - assertEquals("杨浩", BaseHapiFhirDao.normalizeString("杨浩")); - } - @Override protected FhirContext getContext() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index 5e7d3cc046c..eb040fcba84 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -6,7 +6,8 @@ import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; -import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.term.VersionIndependentConcept; import ca.uhn.fhir.jpa.util.ExpungeOptions; import ca.uhn.fhir.jpa.util.JpaConstants; @@ -33,7 +34,9 @@ import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; -import org.mockito.Mockito; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.transaction.PlatformTransactionManager; @@ -75,6 +78,7 @@ public abstract class BaseJpaTest { @Rule public LoggingRule myLoggingRule = new LoggingRule(); + @Mock(answer = Answers.RETURNS_DEEP_STUBS) protected ServletRequestDetails mySrd; protected ArrayList myServerInterceptorList; protected IRequestOperationCallback myRequestOperationCallback = mock(IRequestOperationCallback.class); @@ -89,7 +93,7 @@ public abstract class BaseJpaTest { @After public void afterValidateNoTransaction() { PlatformTransactionManager txManager = getTxManager(); - if (txManager != null) { + if (txManager instanceof JpaTransactionManager) { JpaTransactionManager hibernateTxManager = (JpaTransactionManager) txManager; SessionFactory sessionFactory = (SessionFactory) hibernateTxManager.getEntityManagerFactory(); AtomicBoolean isReadOnly = new AtomicBoolean(); @@ -114,8 +118,9 @@ public abstract class BaseJpaTest { } @Before - public void beforeCreateSrd() { - mySrd = mock(ServletRequestDetails.class, Mockito.RETURNS_DEEP_STUBS); + public void beforeInitMocks() { + MockitoAnnotations.initMocks(this); + when(mySrd.getRequestOperationCallback()).thenReturn(myRequestOperationCallback); myServerInterceptorList = new ArrayList<>(); when(mySrd.getServer().getInterceptors()).thenReturn(myServerInterceptorList); @@ -355,8 +360,9 @@ public abstract class BaseJpaTest { return bundleStr; } - public static void purgeDatabase(DaoConfig theDaoConfig, IFhirSystemDao theSystemDao, ISearchParamPresenceSvc theSearchParamPresenceSvc, ISearchCoordinatorSvc theSearchCoordinatorSvc, ISearchParamRegistry theSearchParamRegistry) { + public static void purgeDatabase(DaoConfig theDaoConfig, IFhirSystemDao theSystemDao, IResourceReindexingSvc theResourceReindexingSvc, ISearchCoordinatorSvc theSearchCoordinatorSvc, ISearchParamRegistry theSearchParamRegistry) { theSearchCoordinatorSvc.cancelAllActiveSearches(); + theResourceReindexingSvc.cancelAndPurgeAllJobs(); boolean expungeEnabled = theDaoConfig.isExpungeEnabled(); theDaoConfig.setExpungeEnabled(true); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java index a1b45f8c4b5..aa28e56d77d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java @@ -7,11 +7,14 @@ import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamStringDao; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.util.ResourceCountCache; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; @@ -42,7 +45,7 @@ import javax.persistence.EntityManager; import java.io.IOException; import java.io.InputStream; -import static org.junit.Assert.*; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @RunWith(SpringJUnit4ClassRunner.class) @@ -56,6 +59,8 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Autowired protected ApplicationContext myAppCtx; @Autowired + protected IResourceReindexingSvc myResourceReindexingSvc; + @Autowired @Qualifier("myAppointmentDaoDstu2") protected IFhirResourceDao myAppointmentDao; @Autowired @@ -73,6 +78,8 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Autowired protected DaoConfig myDaoConfig; @Autowired + protected ModelConfig myModelConfig; + @Autowired @Qualifier("myDeviceDaoDstu2") protected IFhirResourceDao myDeviceDao; @Autowired @@ -197,7 +204,7 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Before @Transactional() public void beforePurgeDatabase() throws InterruptedException { - purgeDatabase(myDaoConfig, mySystemDao, mySearchParamPresenceSvc, mySearchCoordinatorSvc, mySearchParamRegistry); + purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); } @Before diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2InterceptorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2InterceptorTest.java index 3f904baf7b8..c9626785a91 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2InterceptorTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2InterceptorTest.java @@ -23,7 +23,7 @@ import org.mockito.stubbing.Answer; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.*; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; public class FhirResourceDaoDstu2InterceptorTest extends BaseJpaDstu2Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchCustomSearchParamTest.java index 152c8edd74a..90c00800444 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchCustomSearchParamTest.java @@ -1,8 +1,9 @@ package ca.uhn.fhir.jpa.dao.dstu2; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; @@ -32,7 +33,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu @Before public void beforeDisableResultReuse() { myDaoConfig.setReuseCachedSearchResultsForMillis(null); - myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + myDaoConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); } @After @@ -244,7 +245,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu @Test public void testOverrideAndDisableBuiltInSearchParametersWithOverridingDisabled() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(false); + myModelConfig.setDefaultSearchParamsCanBeOverridden(false); SearchParameter memberSp = new SearchParameter(); memberSp.setCode("member"); @@ -286,7 +287,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu @Test public void testOverrideAndDisableBuiltInSearchParametersWithOverridingEnabled() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); + myModelConfig.setDefaultSearchParamsCanBeOverridden(true); SearchParameter memberSp = new SearchParameter(); memberSp.setCode("member"); @@ -988,7 +989,9 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu mySearchParameterDao.delete(spId, mySrd); mySearchParamRegsitry.forceRefresh(); - mySystemDao.performReindexingPass(100); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); // Try with custom gender SP map = new SearchParameterMap(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchFtTest.java index 66ec4548604..7fc9c56374b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchFtTest.java @@ -9,25 +9,22 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl.Suggestion; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.dstu2.resource.*; import ca.uhn.fhir.model.primitive.Base64BinaryDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.util.TestUtil; -import org.springframework.transaction.support.TransactionTemplate; public class FhirResourceDaoDstu2SearchFtTest extends BaseJpaDstu2Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java index 9a393b210f3..7bfd40cfb5c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java @@ -1,9 +1,10 @@ package ca.uhn.fhir.jpa.dao.dstu2; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.model.api.*; import ca.uhn.fhir.model.base.composite.BaseCodingDt; import ca.uhn.fhir.model.dstu2.composite.*; @@ -985,12 +986,12 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { public void testSearchNumberParam() { Encounter e1 = new Encounter(); e1.addIdentifier().setSystem("foo").setValue("testSearchNumberParam01"); - e1.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); + e1.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); IIdType id1 = myEncounterDao.create(e1, mySrd).getId(); Encounter e2 = new Encounter(); e2.addIdentifier().setSystem("foo").setValue("testSearchNumberParam02"); - e2.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("year").setValue(2.0); + e2.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("year").setValue(2.0); IIdType id2 = myEncounterDao.create(e2, mySrd).getId(); { IBundleProvider found = myEncounterDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Encounter.SP_LENGTH, new NumberParam(">2"))); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java index 72d2776a1df..bd46e6178fa 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2Test.java @@ -3,13 +3,15 @@ package ca.uhn.fhir.jpa.dao.dstu2; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.*; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; import org.hamcrest.Matchers; @@ -22,8 +24,8 @@ import org.mockito.ArgumentCaptor; import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoDstu3Test; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; import ca.uhn.fhir.model.api.*; import ca.uhn.fhir.model.base.composite.BaseCodingDt; import ca.uhn.fhir.model.dstu2.composite.*; @@ -2408,17 +2410,17 @@ public class FhirResourceDaoDstu2Test extends BaseJpaDstu2Test { Encounter e1 = new Encounter(); e1.addIdentifier().setSystem("foo").setValue(methodName); - e1.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); + e1.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); IIdType id1 = myEncounterDao.create(e1, mySrd).getId().toUnqualifiedVersionless(); Encounter e3 = new Encounter(); e3.addIdentifier().setSystem("foo").setValue(methodName); - e3.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("year").setValue(3.0); + e3.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("year").setValue(3.0); IIdType id3 = myEncounterDao.create(e3, mySrd).getId().toUnqualifiedVersionless(); Encounter e2 = new Encounter(); e2.addIdentifier().setSystem("foo").setValue(methodName); - e2.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("year").setValue(2.0); + e2.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("year").setValue(2.0); IIdType id2 = myEncounterDao.create(e2, mySrd).getId().toUnqualifiedVersionless(); SearchParameterMap pm; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2UpdateTest.java index 114756207b8..443ccf7a519 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2UpdateTest.java @@ -14,7 +14,7 @@ import org.junit.AfterClass; import org.junit.Test; import org.mockito.ArgumentCaptor; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.*; import ca.uhn.fhir.model.base.composite.BaseCodingDt; import ca.uhn.fhir.model.dstu2.composite.CodingDt; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSearchDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSearchDaoDstu2Test.java index 0ac591fbc17..a7c6583e245 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSearchDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSearchDaoDstu2Test.java @@ -11,7 +11,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.dstu2.resource.Organization; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.rest.api.Constants; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java index f4b96b643bf..8a6d47a1a38 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java @@ -1,8 +1,8 @@ package ca.uhn.fhir.jpa.dao.dstu2; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 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 9737d3593fb..25f1dd08211 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 @@ -6,12 +6,15 @@ import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.data.*; import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.term.BaseHapiTerminologySvcImpl; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; @@ -67,6 +70,10 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { @Qualifier("myResourceCountsCache") protected ResourceCountCache myResourceCountsCache; @Autowired + protected IResourceReindexingSvc myResourceReindexingSvc; + @Autowired + protected IResourceReindexJobDao myResourceReindexJobDao; + @Autowired @Qualifier("myCoverageDaoDstu3") protected IFhirResourceDao myCoverageDao; @Autowired @@ -103,6 +110,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { @Autowired protected DaoConfig myDaoConfig; @Autowired + protected ModelConfig myModelConfig; + @Autowired @Qualifier("myDeviceDaoDstu3") protected IFhirResourceDao myDeviceDao; @Autowired @@ -247,6 +256,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { protected ITermConceptMapGroupElementTargetDao myTermConceptMapGroupElementTargetDao; @Autowired private JpaValidationSupportChainDstu3 myJpaValidationSupportChainDstu3; + @Autowired + protected ISearchParamRegistry mySearchParamRegistry; @After() public void afterCleanupDao() { @@ -294,8 +305,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { @Before @Transactional() - public void beforePurgeDatabase() throws InterruptedException { - purgeDatabase(myDaoConfig, mySystemDao, mySearchParamPresenceSvc, mySearchCoordinatorSvc, mySearchParamRegsitry); + public void beforePurgeDatabase() { + purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegsitry); } @Before diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCustomTypeDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCustomTypeDstu3Test.java index 6aba4204cd6..9362d9abdb0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCustomTypeDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoCustomTypeDstu3Test.java @@ -6,7 +6,7 @@ import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; @SuppressWarnings({ }) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java index 36dd96b73d0..1c84d1a3868 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java @@ -30,10 +30,9 @@ public class FhirResourceDaoDstu3CodeSystemTest extends BaseJpaDstu3Test { CodeSystem cs = myFhirCtx.newJsonParser().parseResource(CodeSystem.class, input); myCodeSystemDao.create(cs, mySrd); - - mySystemDao.markAllResourcesForReindexing(); - int outcome = mySystemDao.performReindexingPass(100); + myResourceReindexingSvc.markAllResourcesForReindexing(); + int outcome= myResourceReindexingSvc.forceReindexingPass(); assertNotEquals(-1, outcome); // -1 means there was a failure myTermSvc.saveDeferred(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ContainedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ContainedTest.java index cd2c9c8fcc7..941900ae49d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ContainedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ContainedTest.java @@ -7,7 +7,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.junit.AfterClass; import org.junit.Test; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.util.TestUtil; public class FhirResourceDaoDstu3ContainedTest extends BaseJpaDstu3Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ExternalReferenceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ExternalReferenceTest.java index 41d785d2174..7eaa1aa5351 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ExternalReferenceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ExternalReferenceTest.java @@ -18,7 +18,7 @@ import org.junit.Before; import org.junit.Test; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.TestUtil; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3InterceptorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3InterceptorTest.java index 170c6a40dc8..69f72cf4004 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3InterceptorTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3InterceptorTest.java @@ -24,7 +24,7 @@ import org.mockito.stubbing.Answer; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.*; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; public class FhirResourceDaoDstu3InterceptorTest extends BaseJpaDstu3Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java index 7eb293d9418..cb52de87033 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; @@ -994,7 +994,8 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu mySearchParameterDao.delete(spId, mySrd); mySearchParamRegsitry.forceRefresh(); - mySystemDao.performReindexingPass(100); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); // Try with custom gender SP map = new SearchParameterMap(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchFtTest.java index 477d8da8c65..1593dce462e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchFtTest.java @@ -9,21 +9,17 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.Observation.ObservationStatus; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl.Suggestion; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.TestUtil; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; public class FhirResourceDaoDstu3SearchFtTest extends BaseJpaDstu3Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java index 10953058302..5ebda85d4df 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java @@ -11,6 +11,8 @@ import java.util.*; import javax.servlet.http.HttpServletRequest; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.dstu3.model.*; @@ -27,8 +29,8 @@ import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.StrictErrorHandler; @@ -1197,7 +1199,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { int sleep = 100; long start = System.currentTimeMillis(); - Thread.sleep(sleep); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); IIdType id1a; { @@ -1216,7 +1218,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { ourLog.info("Res 2: {}", myPatientDao.read(id1a, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); ourLog.info("Res 3: {}", myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); - Thread.sleep(sleep); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); long end = System.currentTimeMillis(); SearchParameterMap map; @@ -1318,12 +1320,12 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { public void testSearchNumberParam() { Encounter e1 = new Encounter(); e1.addIdentifier().setSystem("foo").setValue("testSearchNumberParam01"); - e1.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); + e1.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); IIdType id1 = myEncounterDao.create(e1, mySrd).getId(); Encounter e2 = new Encounter(); e2.addIdentifier().setSystem("foo").setValue("testSearchNumberParam02"); - e2.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("year").setValue(2.0); + e2.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("year").setValue(2.0); IIdType id2 = myEncounterDao.create(e2, mySrd).getId(); { IBundleProvider found = myEncounterDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Encounter.SP_LENGTH, new NumberParam(">2"))); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java index ee4f62ab945..90b19eaaa53 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4SearchPageExpiryTest; import ca.uhn.fhir.jpa.entity.Search; 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 f1635fbdb0c..00e0ee2efa4 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 @@ -5,6 +5,9 @@ import ca.uhn.fhir.jpa.config.TestDstu3WithoutLuceneConfig; import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -144,12 +147,12 @@ public class FhirResourceDaoDstu3SearchWithLuceneDisabledTest extends BaseJpaTes @Autowired @Qualifier("myJpaValidationSupportChainDstu3") private IValidationSupport myValidationSupport; + @Autowired + private IResourceReindexingSvc myResourceReindexingSvc; @Before public void beforePurgeDatabase() { - runInTransaction(() -> { - purgeDatabase(myDaoConfig, mySystemDao, mySearchParamPresenceSvc, 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/FhirResourceDaoDstu3TerminologyTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java index 1355c5ded91..b03e0b09148 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java @@ -22,7 +22,8 @@ import org.springframework.beans.factory.annotation.Autowired; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem.LookupCodeResult; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; @@ -487,10 +488,10 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { createExternalCsAndLocalVs(); - mySystemDao.markAllResourcesForReindexing(); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); - mySystemDao.performReindexingPass(100); - mySystemDao.performReindexingPass(100); myHapiTerminologySvc.saveDeferred(); myHapiTerminologySvc.saveDeferred(); myHapiTerminologySvc.saveDeferred(); @@ -729,17 +730,17 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { include.setSystem(URL_MY_CODE_SYSTEM); include.addConcept().setCode("ZZZZ"); - mySystemDao.markAllResourcesForReindexing(); - mySystemDao.performReindexingPass(null); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); myTermSvc.saveDeferred(); - mySystemDao.performReindexingPass(null); myTermSvc.saveDeferred(); // Again - mySystemDao.markAllResourcesForReindexing(); - mySystemDao.performReindexingPass(null); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); myTermSvc.saveDeferred(); - mySystemDao.performReindexingPass(null); myTermSvc.saveDeferred(); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java index 7bfd175aec8..d8b94a50f9f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java @@ -1,8 +1,10 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; @@ -44,7 +46,7 @@ import static org.apache.commons.lang3.StringUtils.defaultString; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @SuppressWarnings({"unchecked", "deprecation"}) @@ -2917,17 +2919,17 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { Encounter e1 = new Encounter(); e1.addIdentifier().setSystem("foo").setValue(methodName); - e1.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); + e1.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); IIdType id1 = myEncounterDao.create(e1, mySrd).getId().toUnqualifiedVersionless(); Encounter e3 = new Encounter(); e3.addIdentifier().setSystem("foo").setValue(methodName); - e3.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("year").setValue(3.0); + e3.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("year").setValue(3.0); IIdType id3 = myEncounterDao.create(e3, mySrd).getId().toUnqualifiedVersionless(); Encounter e2 = new Encounter(); e2.addIdentifier().setSystem("foo").setValue(methodName); - e2.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("year").setValue(2.0); + e2.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("year").setValue(2.0); IIdType id2 = myEncounterDao.create(e2, mySrd).getId().toUnqualifiedVersionless(); SearchParameterMap pm; 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 be99f609aa3..2fb1336f5f0 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 @@ -1,10 +1,11 @@ package ca.uhn.fhir.jpa.dao.dstu3; -import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.SearchBuilder; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; -import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.util.JpaConstants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.DateParam; @@ -20,7 +21,6 @@ import org.junit.Before; import org.junit.Test; import org.springframework.test.context.TestPropertySource; -import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -39,12 +39,12 @@ public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test @After public void after() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); } @Before public void before() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); + myModelConfig.setDefaultSearchParamsCanBeOverridden(true); } private void createUniqueBirthdateAndGenderSps() { @@ -78,7 +78,7 @@ public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test .setExpression("Patient") .setDefinition(new Reference("SearchParameter/patient-birthdate")); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); @@ -118,7 +118,7 @@ public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test .setExpression("Coverage") .setDefinition(new Reference("/SearchParameter/coverage-identifier")); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); mySearchParamRegsitry.forceRefresh(); @@ -155,7 +155,7 @@ public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test .setExpression("Patient") .setDefinition(new Reference("SearchParameter/patient-organization")); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); @@ -208,141 +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(); - - mySystemDao.markAllResourcesForReindexing(); - mySystemDao.performReindexingPass(1000); - - 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(); @@ -472,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/FhirResourceDaoDstu3UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java index edda2a99c70..51089f210d4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java @@ -15,7 +15,7 @@ import org.junit.*; import org.mockito.ArgumentCaptor; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; @@ -306,6 +306,7 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test { assertEquals("1", outcome.getId().getVersionIdPart()); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); Date now = new Date(); Patient retrieved = myPatientDao.read(outcome.getId(), mySrd); InstantType updated = retrieved.getMeta().getLastUpdatedElement().copy(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSearchDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSearchDaoDstu3Test.java index 0fb5b997f08..2e54e5dd1fe 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSearchDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSearchDaoDstu3Test.java @@ -13,7 +13,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.util.TestUtil; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java index effc8d746bd..7f303c9a586 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java @@ -2,12 +2,14 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.ResourceTag; -import ca.uhn.fhir.jpa.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.dao.GZipUtil; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.model.entity.ResourceTag; +import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.parser.LenientErrorHandler; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -322,6 +324,20 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { } + + @Test + @Ignore + public void testProcessCollectionAsBatch() throws IOException { + byte[] inputBytes = IOUtils.toByteArray(getClass().getResourceAsStream("/dstu3/Reilly_Libby_73.json.gz")); + String input = GZipUtil.decompress(inputBytes); + Bundle bundle = myFhirCtx.newJsonParser().setParserErrorHandler(new LenientErrorHandler()).parseResource(Bundle.class, input); + ourLog.info("Bundle has {} resources", bundle); + + Bundle output = mySystemDao.transaction(mySrd, bundle); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + } + + /** * See #410 */ @@ -3040,7 +3056,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { @Test public void testTransactionWithReplacement() { - byte[] bytes = new byte[] {0, 1, 2, 3, 4}; + byte[] bytes = new byte[]{0, 1, 2, 3, 4}; Binary binary = new Binary(); binary.setId(IdType.newRandomUuid()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index bbfc58077fd..b07b4b3f281 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -5,13 +5,16 @@ import ca.uhn.fhir.jpa.config.TestR4Config; import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.data.*; import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.term.BaseHapiTerminologySvcImpl; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; @@ -115,6 +118,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Autowired protected DaoConfig myDaoConfig; @Autowired + protected ModelConfig myModelConfig; + @Autowired @Qualifier("myDeviceDaoR4") protected IFhirResourceDao myDeviceDao; @Autowired @@ -213,6 +218,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Autowired protected ISearchIncludeDao mySearchIncludeDao; @Autowired + protected IResourceReindexJobDao myResourceReindexJobDao; + @Autowired @Qualifier("mySearchParameterDaoR4") protected IFhirResourceDao mySearchParameterDao; @Autowired @@ -237,6 +244,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Qualifier("mySystemDaoR4") protected IFhirSystemDao mySystemDao; @Autowired + protected IResourceReindexingSvc myResourceReindexingSvc; + @Autowired @Qualifier("mySystemProviderR4") protected JpaSystemProviderR4 mySystemProvider; @Autowired @@ -314,7 +323,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Transactional() public void beforePurgeDatabase() throws InterruptedException { final EntityManager entityManager = this.myEntityManager; - purgeDatabase(myDaoConfig, mySystemDao, mySearchParamPresenceSvc, mySearchCoordinatorSvc, mySearchParamRegsitry); + purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegsitry); } @Before diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java index b74c2aadebb..57553c26da6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java @@ -1,57 +1,26 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.entity.TagTypeEnum; -import ca.uhn.fhir.model.api.Include; -import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; -import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.*; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.exceptions.*; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.TestUtil; -import com.google.common.base.Charsets; -import com.google.common.collect.Lists; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.RandomStringUtils; -import org.hamcrest.Matchers; -import org.hamcrest.core.StringContains; -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.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.Bundle.BundleType; -import org.hl7.fhir.r4.model.Bundle.HTTPVerb; -import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; -import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; -import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; -import org.hl7.fhir.r4.model.OperationOutcome.IssueType; -import org.hl7.fhir.r4.model.Quantity.QuantityComparator; -import org.junit.*; -import org.mockito.ArgumentCaptor; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; +import org.hl7.fhir.r4.model.Task; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; -import java.util.*; +import java.util.List; -import static org.apache.commons.lang3.StringUtils.defaultString; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; -@SuppressWarnings({ "unchecked", "deprecation" }) +@SuppressWarnings({"unchecked", "deprecation"}) public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoCreatePlaceholdersR4Test.class); @@ -59,6 +28,7 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { @After public final void afterResetDao() { myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets()); + myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy()); } @Test @@ -131,7 +101,7 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { } @Test - public void testUpdateWithBadReferenceIsPermitted() { + public void testUpdateWithBadReferenceIsPermittedAlphanumeric() { assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets()); myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); @@ -139,11 +109,49 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { o.setStatus(ObservationStatus.FINAL); IIdType id = myObservationDao.create(o, mySrd).getId(); + try { + myPatientDao.read(new IdType("Patient/FOO")); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + o = new Observation(); o.setId(id); o.setStatus(ObservationStatus.FINAL); o.getSubject().setReference("Patient/FOO"); myObservationDao.update(o, mySrd); + + myPatientDao.read(new IdType("Patient/FOO")); + + } + + @Test + public void testUpdateWithBadReferenceIsPermittedNumeric() { + assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets()); + myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); + myDaoConfig.setResourceClientIdStrategy(DaoConfig.ClientIdStrategyEnum.ANY); + + Observation o = new Observation(); + o.setStatus(ObservationStatus.FINAL); + IIdType id = myObservationDao.create(o, mySrd).getId(); + + try { + myPatientDao.read(new IdType("Patient/999999999999999")); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + + o = new Observation(); + o.setId(id); + o.setStatus(ObservationStatus.FINAL); + o.getSubject().setReference("Patient/999999999999999"); + myObservationDao.update(o, mySrd); + + + myPatientDao.read(new IdType("Patient/999999999999999")); + } @AfterClass diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCustomTypeR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCustomTypeR4Test.java index 0b36153b727..b00b92de015 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCustomTypeR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCustomTypeR4Test.java @@ -6,7 +6,7 @@ import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; @SuppressWarnings({ }) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CacheWarmingTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CacheWarmingTest.java index c71c6af5ce4..7488bcc4b14 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CacheWarmingTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CacheWarmingTest.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl; import ca.uhn.fhir.jpa.search.warm.WarmCacheEntry; @@ -16,7 +16,6 @@ import org.junit.AfterClass; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CodeSystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CodeSystemTest.java index f334e53d7ac..a44c132d352 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CodeSystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CodeSystemTest.java @@ -30,10 +30,8 @@ public class FhirResourceDaoR4CodeSystemTest extends BaseJpaR4Test { CodeSystem cs = myFhirCtx.newJsonParser().parseResource(CodeSystem.class, input); myCodeSystemDao.create(cs, mySrd); - - mySystemDao.markAllResourcesForReindexing(); - - int outcome = mySystemDao.performReindexingPass(100); + myResourceReindexingSvc.markAllResourcesForReindexing(); + int outcome = myResourceReindexingSvc.forceReindexingPass(); assertNotEquals(-1, outcome); // -1 means there was a failure myTermSvc.saveDeferred(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java index 39a279b991f..9bc8e9b4783 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java @@ -7,7 +7,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.junit.AfterClass; import org.junit.Test; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.util.TestUtil; public class FhirResourceDaoR4ContainedTest extends BaseJpaR4Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java index 4006322d6bc..bdafcd8d3e9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java @@ -1,25 +1,26 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.TestUtil; +import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Organization; -import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.*; import org.junit.After; import org.junit.AfterClass; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.PageRequest; import java.io.IOException; +import java.util.Date; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4CreateTest.class); @@ -27,6 +28,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { @After public void afterResetDao() { myDaoConfig.setResourceServerIdStrategy(new DaoConfig().getResourceServerIdStrategy()); + myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy()); } @Test @@ -72,6 +74,134 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { } + @Test + public void testCreateWithClientAssignedIdDisallowed() { + myDaoConfig.setResourceClientIdStrategy(DaoConfig.ClientIdStrategyEnum.NOT_ALLOWED); + + Patient p = new Patient(); + p.setId("AAA"); + p.addName().setFamily("FAM"); + try { + myPatientDao.update(p); + fail(); + } catch (ResourceNotFoundException e) { + assertEquals("No resource exists on this server resource with ID[AAA], and client-assigned IDs are not enabled.", e.getMessage()); + } + } + + @Test + public void testCreateWithClientAssignedIdPureNumeric() { + myDaoConfig.setResourceServerIdStrategy(DaoConfig.IdStrategyEnum.SEQUENTIAL_NUMERIC); + myDaoConfig.setResourceClientIdStrategy(DaoConfig.ClientIdStrategyEnum.ANY); + + // Create a server assigned ID + Patient p = new Patient(); + p.setActive(true); + IIdType id0 = myPatientDao.create(p).getId(); + long firstClientAssignedId = id0.getIdPartAsLong(); + long newId = firstClientAssignedId + 2L; + + // Read it back + p = myPatientDao.read(new IdType("Patient/" + firstClientAssignedId)); + assertEquals(true, p.getActive()); + + // Not create a client assigned numeric ID + p = new Patient(); + p.setId("Patient/" + newId); + p.addName().setFamily("FAM"); + IIdType id1 = myPatientDao.update(p).getId(); + + assertEquals(Long.toString(newId), id1.getIdPart()); + assertEquals("1", id1.getVersionIdPart()); + + p = myPatientDao.read(id1); + assertEquals("FAM", p.getNameFirstRep().getFamily()); + + // Update it + p = new Patient(); + p.setId("Patient/" + newId); + p.addName().setFamily("FAM2"); + id1 = myPatientDao.update(p).getId(); + + assertEquals(Long.toString(newId), id1.getIdPart()); + assertEquals("2", id1.getVersionIdPart()); + + p = myPatientDao.read(id1); + assertEquals("FAM2", p.getNameFirstRep().getFamily()); + + // Try to create another server-assigned. This should fail since we have a + // a conflict. + p = new Patient(); + p.setActive(false); + try { + myPatientDao.create(p).getId(); + fail(); + } catch (DataIntegrityViolationException e) { + // good + } + + ourLog.info("ID0: {}", id0); + ourLog.info("ID1: {}", id1); + } + + @Test + public void testCreateWithClientAssignedIdPureNumericServerIdUuid() { + myDaoConfig.setResourceServerIdStrategy(DaoConfig.IdStrategyEnum.UUID); + myDaoConfig.setResourceClientIdStrategy(DaoConfig.ClientIdStrategyEnum.ANY); + + // Create a server assigned ID + Patient p = new Patient(); + p.setActive(true); + IIdType id0 = myPatientDao.create(p).getId(); + + // Read it back + p = myPatientDao.read(id0.toUnqualifiedVersionless()); + assertEquals(true, p.getActive()); + + // Pick an ID that was already used as an internal PID + Long newId = runInTransaction(() -> myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromNewest( + PageRequest.of(0, 1), + DateUtils.addDays(new Date(), -1), + DateUtils.addDays(new Date(), 1) + ).getContent().get(0)); + + // Not create a client assigned numeric ID + p = new Patient(); + p.setId("Patient/" + newId); + p.addName().setFamily("FAM"); + IIdType id1 = myPatientDao.update(p).getId(); + + assertEquals(Long.toString(newId), id1.getIdPart()); + assertEquals("1", id1.getVersionIdPart()); + + // Read it back + p = myPatientDao.read(id1); + assertEquals("FAM", p.getNameFirstRep().getFamily()); + + // Update it + p = new Patient(); + p.setId("Patient/" + newId); + p.addName().setFamily("FAM2"); + id1 = myPatientDao.update(p).getId(); + + assertEquals(Long.toString(newId), id1.getIdPart()); + assertEquals("2", id1.getVersionIdPart()); + + p = myPatientDao.read(id1); + assertEquals("FAM2", p.getNameFirstRep().getFamily()); + + // Try to create another server-assigned. This should fail since we have a + // a conflict. + p = new Patient(); + p.setActive(false); + IIdType id2 = myPatientDao.create(p).getId(); + + ourLog.info("ID0: {}", id0); + ourLog.info("ID1: {}", id1); + ourLog.info("ID2: {}", id2); + } + + @Test public void testTransactionCreateWithUuidResourceStrategy() { myDaoConfig.setResourceServerIdStrategy(DaoConfig.IdStrategyEnum.UUID); @@ -83,6 +213,8 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { Patient p = new Patient(); p.setId(IdType.newRandomUuid()); p.addName().setFamily("FAM"); + p.setActive(true); + p.setBirthDateElement(new DateType("2011-01-01")); p.getManagingOrganization().setReference(org.getId()); Bundle input = new Bundle(); 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..1ea61ad2067 --- /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.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.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/FhirResourceDaoR4ExternalReferenceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ExternalReferenceTest.java index 58298c72404..90f2afc95d4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ExternalReferenceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ExternalReferenceTest.java @@ -18,7 +18,7 @@ import org.junit.Before; import org.junit.Test; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.TestUtil; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index dfcbc1c61a8..4f75f7f370a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -1,14 +1,18 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.util.TestUtil; import net.ttddyy.dsproxy.QueryCount; -import net.ttddyy.dsproxy.QueryCountHolder; import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.junit.After; import org.junit.AfterClass; @@ -64,9 +68,9 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { ourLog.info("** Done performing write 2"); - assertEquals(2, getQueryCount().getInsert()); - assertEquals(1, getQueryCount().getUpdate()); - assertEquals(1, getQueryCount().getDelete()); + assertEquals(1, getQueryCount().getInsert()); + assertEquals(2, getQueryCount().getUpdate()); + assertEquals(0, getQueryCount().getDelete()); } @Test @@ -129,7 +133,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field myPatientDao.update(p).getId().toUnqualifiedVersionless(); - assertEquals(5, getQueryCount().getSelect()); + assertEquals(4, getQueryCount().getSelect()); assertEquals(1, getQueryCount().getInsert()); assertEquals(0, getQueryCount().getDelete()); assertEquals(1, getQueryCount().getUpdate()); @@ -158,6 +162,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { assertEquals(1, myResourceHistoryTableDao.count()); }); + + myCountHolder.clear(); p = new Patient(); p.setId(id); @@ -184,8 +190,6 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { pt.addName().setFamily("FAMILY1").addGiven("GIVEN1A").addGiven("GIVEN1B"); IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless(); - ourLog.info("Now have {} deleted", getQueryCount().getDelete()); - ourLog.info("Now have {} inserts", getQueryCount().getInsert()); myCountHolder.clear(); ourLog.info("** About to update"); @@ -194,13 +198,148 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { pt.getNameFirstRep().addGiven("GIVEN1C"); myPatientDao.update(pt); - ourLog.info("Now have {} deleted", getQueryCount().getDelete()); - ourLog.info("Now have {} inserts", getQueryCount().getInsert()); assertEquals(0, getQueryCount().getDelete()); assertEquals(2, getQueryCount().getInsert()); } + @Test + public void testUpdateReusesIndexesString() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + SearchParameterMap m1 = new SearchParameterMap().add("family", new StringParam("family1")).setLoadSynchronous(true); + SearchParameterMap m2 = new SearchParameterMap().add("family", new StringParam("family2")).setLoadSynchronous(true); + + myCountHolder.clear(); + + Patient pt = new Patient(); + pt.addName().setFamily("FAMILY1"); + IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless(); + + myCountHolder.clear(); + + assertEquals(1, myPatientDao.search(m1).size().intValue()); + assertEquals(0, myPatientDao.search(m2).size().intValue()); + + ourLog.info("** About to update"); + + pt = new Patient(); + pt.setId(id); + pt.addName().setFamily("FAMILY2"); + myPatientDao.update(pt); + + assertEquals(0, getQueryCount().getDelete()); + assertEquals(1, getQueryCount().getInsert()); // Add an entry to HFJ_RES_VER + assertEquals(2, getQueryCount().getUpdate()); // Update SPIDX_STRING and HFJ_RESOURCE + + assertEquals(0, myPatientDao.search(m1).size().intValue()); + assertEquals(1, myPatientDao.search(m2).size().intValue()); + } + + + @Test + public void testUpdateReusesIndexesToken() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + SearchParameterMap m1 = new SearchParameterMap().add("gender", new TokenParam("male")).setLoadSynchronous(true); + SearchParameterMap m2 = new SearchParameterMap().add("gender", new TokenParam("female")).setLoadSynchronous(true); + + myCountHolder.clear(); + + Patient pt = new Patient(); + pt.setGender(Enumerations.AdministrativeGender.MALE); + IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless(); + + assertEquals(0, getQueryCount().getSelect()); + assertEquals(0, getQueryCount().getDelete()); + assertEquals(3, getQueryCount().getInsert()); + assertEquals(0, getQueryCount().getUpdate()); + assertEquals(1, myPatientDao.search(m1).size().intValue()); + assertEquals(0, myPatientDao.search(m2).size().intValue()); + + /* + * Change a value + */ + + ourLog.info("** About to update"); + myCountHolder.clear(); + + pt = new Patient(); + pt.setId(id); + pt.setGender(Enumerations.AdministrativeGender.FEMALE); + myPatientDao.update(pt); + + /* + * Current SELECTs: + * Select the resource from HFJ_RESOURCE + * Select the version from HFJ_RES_VER + * Select the current token indexes + */ + assertEquals(3, getQueryCount().getSelect()); + assertEquals(0, getQueryCount().getDelete()); + assertEquals(1, getQueryCount().getInsert()); // Add an entry to HFJ_RES_VER + assertEquals(2, getQueryCount().getUpdate()); // Update SPIDX_STRING and HFJ_RESOURCE + + assertEquals(0, myPatientDao.search(m1).size().intValue()); + assertEquals(1, myPatientDao.search(m2).size().intValue()); + myCountHolder.clear(); + + /* + * Drop a value + */ + + ourLog.info("** About to update again"); + + pt = new Patient(); + pt.setId(id); + myPatientDao.update(pt); + + assertEquals(1, getQueryCount().getDelete()); + assertEquals(1, getQueryCount().getInsert()); + assertEquals(1, getQueryCount().getUpdate()); + + assertEquals(0, myPatientDao.search(m1).size().intValue()); + assertEquals(0, myPatientDao.search(m2).size().intValue()); + + } + + @Test + public void testUpdateReusesIndexesResourceLink() { + Organization org1 = new Organization(); + org1.setName("org1"); + IIdType orgId1 = myOrganizationDao.create(org1).getId().toUnqualifiedVersionless(); + Organization org2 = new Organization(); + org2.setName("org2"); + IIdType orgId2 = myOrganizationDao.create(org2).getId().toUnqualifiedVersionless(); + + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + SearchParameterMap m1 = new SearchParameterMap().add("organization", new ReferenceParam(orgId1.getValue())).setLoadSynchronous(true); + SearchParameterMap m2 = new SearchParameterMap().add("organization", new ReferenceParam(orgId2.getValue())).setLoadSynchronous(true); + + myCountHolder.clear(); + + Patient pt = new Patient(); + pt.getManagingOrganization().setReference(orgId1.getValue()); + IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless(); + + myCountHolder.clear(); + + assertEquals(1, myPatientDao.search(m1).size().intValue()); + assertEquals(0, myPatientDao.search(m2).size().intValue()); + + ourLog.info("** About to update"); + + pt = new Patient(); + pt.setId(id); + pt.getManagingOrganization().setReference(orgId2.getValue()); + myPatientDao.update(pt); + + assertEquals(0, getQueryCount().getDelete()); + assertEquals(1, getQueryCount().getInsert()); // Add an entry to HFJ_RES_VER + assertEquals(2, getQueryCount().getUpdate()); // Update SPIDX_STRING and HFJ_RESOURCE + + assertEquals(0, myPatientDao.search(m1).size().intValue()); + assertEquals(1, myPatientDao.search(m2).size().intValue()); + } + @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 6c988c367e7..612f8256aa2 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 @@ -1,12 +1,12 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.TestUtil; @@ -14,10 +14,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Appointment.AppointmentStatus; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; import org.mockito.internal.util.collections.ListUtil; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; @@ -40,7 +37,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test @Before public void beforeDisableResultReuse() { myDaoConfig.setReuseCachedSearchResultsForMillis(null); - myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); } @Test @@ -61,6 +58,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test } @Test + @Ignore public void testCreateInvalidParamInvalidResourceName() { SearchParameter fooSp = new SearchParameter(); fooSp.addBase("Patient"); @@ -96,6 +94,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test } @Test + @Ignore public void testCreateInvalidParamNoResourceName() { SearchParameter fooSp = new SearchParameter(); fooSp.addBase("Patient"); @@ -146,8 +145,9 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test mySearchParameterDao.create(fooSp, mySrd); - assertEquals(1, mySystemDao.performReindexingPass(100).intValue()); - assertEquals(0, mySystemDao.performReindexingPass(100).intValue()); + assertEquals(1, myResourceReindexingSvc.forceReindexingPass()); + assertEquals(1, myResourceReindexingSvc.forceReindexingPass()); + assertEquals(0, myResourceReindexingSvc.forceReindexingPass()); } @@ -243,33 +243,11 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test } - @Test - public void testIndexFailsIfInvalidSearchParameterExists() { - myDaoConfig.setValidateSearchParameterExpressionsOnSave(false); - SearchParameter threadIdSp = new SearchParameter(); - threadIdSp.addBase("Communication"); - threadIdSp.setCode("has-attachments"); - threadIdSp.setType(Enumerations.SearchParamType.REFERENCE); - threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null"); - threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); - threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE); - mySearchParameterDao.create(threadIdSp, mySrd); - mySearchParamRegsitry.forceRefresh(); - - Communication com = new Communication(); - com.setStatus(Communication.CommunicationStatus.INPROGRESS); - try { - myCommunicationDao.create(com, mySrd); - fail(); - } catch (InternalErrorException e) { - assertThat(e.getMessage(), startsWith("Failed to extract values from resource using FHIRPath \"Communication.payload[1].contentAttachment is not null\": org.hl7.fhir")); - } - } @Test public void testOverrideAndDisableBuiltInSearchParametersWithOverridingDisabled() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(false); + myModelConfig.setDefaultSearchParamsCanBeOverridden(false); SearchParameter memberSp = new SearchParameter(); memberSp.setCode("member"); @@ -309,7 +287,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test @Test public void testOverrideAndDisableBuiltInSearchParametersWithOverridingEnabled() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); + myModelConfig.setDefaultSearchParamsCanBeOverridden(true); SearchParameter memberSp = new SearchParameter(); memberSp.setCode("member"); @@ -430,7 +408,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test mySearchParameterDao.create(threadIdSp, mySrd); fail(); } catch (UnprocessableEntityException e) { - assertThat(e.getMessage(), startsWith("The expression \"Communication.payload[1].contentAttachment is not null\" can not be evaluated and may be invalid: ")); + assertThat(e.getMessage(), startsWith("Invalid SearchParameter.expression value \"Communication.payload[1].contentAttachment is not null\"")); } } @@ -1171,7 +1149,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test mySearchParameterDao.delete(spId, mySrd); mySearchParamRegsitry.forceRefresh(); - mySystemDao.performReindexingPass(100); + myResourceReindexingSvc.forceReindexingPass(); // Try with custom gender SP map = new SearchParameterMap(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchFtTest.java index 4c5b7fa01be..c5bc029e0ef 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchFtTest.java @@ -9,21 +9,17 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.*; import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl.Suggestion; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.TestUtil; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchMissingTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchMissingTest.java index ea95aa93052..e68986912ce 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchMissingTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchMissingTest.java @@ -1,10 +1,9 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index cbc72081089..28e26d43be6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -1,9 +1,9 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.StrictErrorHandler; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java index 9c3395469fc..28104c035ed 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java @@ -1,9 +1,9 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.StrictErrorHandler; @@ -1350,7 +1350,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { String methodName = "testSearchLastUpdatedParam"; int sleep = 100; - Thread.sleep(sleep); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); DateTimeType beforeAny = new DateTimeType(new Date(), TemporalPrecisionEnum.MILLI); IIdType id1a; @@ -1368,9 +1368,9 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { id1b = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - Thread.sleep(1100); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1100); DateTimeType beforeR2 = new DateTimeType(new Date(), TemporalPrecisionEnum.MILLI); - Thread.sleep(1100); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1100); IIdType id2; { @@ -1444,7 +1444,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { int sleep = 100; long start = System.currentTimeMillis(); - Thread.sleep(sleep); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); IIdType id1a; { @@ -1463,7 +1463,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { ourLog.info("Res 2: {}", myPatientDao.read(id1a, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); ourLog.info("Res 3: {}", myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); - Thread.sleep(sleep); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); long end = System.currentTimeMillis(); SearchParameterMap map; @@ -1860,14 +1860,14 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId().toUnqualifiedVersionless(); Date between = new Date(); - Thread.sleep(10); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(10); Observation obs02 = new Observation(); obs02.setEffective(new DateTimeType(new Date())); obs02.setSubject(new Reference(locId01)); IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId().toUnqualifiedVersionless(); - Thread.sleep(10); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(10); Date after = new Date(); ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", patientId01, locId01, obsId01, obsId02); @@ -1991,14 +1991,14 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } Date between = new Date(); - Thread.sleep(10); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(10); { Patient patient = new Patient(); patient.addIdentifier().setSystem("urn:system").setValue("002"); patient.addName().setFamily("Tester_testSearchStringParam").addGiven("John"); pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - Thread.sleep(10); + ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(10); Date after = new Date(); SearchParameterMap params; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java index cb9adf61089..d55027b1949 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchOptimizedTest.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchStatusEnum; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; @@ -12,7 +12,6 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.util.TestUtil; -import com.google.common.collect.Sets; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.r4.model.Patient; import org.junit.After; @@ -25,7 +24,6 @@ import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java index 9b22d036883..aa99e4dbb4b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchStatusEnum; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java index 650d4f90b11..cbfafce6423 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java @@ -4,6 +4,9 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.config.TestR4WithoutLuceneConfig; import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -89,11 +92,13 @@ public class FhirResourceDaoR4SearchWithLuceneDisabledTest extends BaseJpaTest { private IValidationSupport myValidationSupport; @Autowired private IFhirSystemDao mySystemDao; + @Autowired + private IResourceReindexingSvc myResourceReindexingSvc; @Before @Transactional() public void beforePurgeDatabase() { - purgeDatabase(myDaoConfig, mySystemDao, mySearchParamPresenceSvc, mySearchCoordinatorSvc, mySearchParamRegistry); + purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); } @Before diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SortTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SortTest.java index e74af3b5b27..fd142e85eb1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SortTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SortTest.java @@ -1,7 +1,6 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java index c0593617ad9..98102f6c0d3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java @@ -2,8 +2,8 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem.LookupCodeResult; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; @@ -539,10 +539,9 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { createExternalCsAndLocalVs(); - mySystemDao.markAllResourcesForReindexing(); - - mySystemDao.performReindexingPass(100); - mySystemDao.performReindexingPass(100); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); myHapiTerminologySvc.saveDeferred(); myHapiTerminologySvc.saveDeferred(); myHapiTerminologySvc.saveDeferred(); @@ -851,17 +850,17 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { include.setSystem(URL_MY_CODE_SYSTEM); include.addConcept().setCode("ZZZZ"); - mySystemDao.markAllResourcesForReindexing(); - mySystemDao.performReindexingPass(null); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); myTermSvc.saveDeferred(); - mySystemDao.performReindexingPass(null); myTermSvc.saveDeferred(); // Again - mySystemDao.markAllResourcesForReindexing(); - mySystemDao.performReindexingPass(null); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); myTermSvc.saveDeferred(); - mySystemDao.performReindexingPass(null); myTermSvc.saveDeferred(); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java index 2b94014d187..59c80701a1a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java @@ -1,8 +1,15 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.entity.Search; +import ca.uhn.fhir.jpa.entity.SearchStatusEnum; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; @@ -53,7 +60,7 @@ import static org.apache.commons.lang3.StringUtils.defaultString; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @SuppressWarnings({"unchecked", "deprecation", "Duplicates"}) @@ -162,6 +169,9 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { runInTransaction(() -> { assertThat(myResourceIndexedSearchParamTokenDao.countForResourceId(id1.getIdPartAsLong()), greaterThan(0)); + Optional tableOpt = myResourceTableDao.findById(id1.getIdPartAsLong()); + assertTrue(tableOpt.isPresent()); + assertEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXED, tableOpt.get().getIndexStatus().longValue()); }); runInTransaction(() -> { @@ -170,10 +180,16 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { ResourceTable table = tableOpt.get(); table.setIndexStatus(null); table.setDeleted(new Date()); + table = myResourceTableDao.saveAndFlush(table); + ResourceHistoryTable newHistory = table.toHistory(); + ResourceHistoryTable currentHistory = myResourceHistoryTableDao.findForIdAndVersion(table.getId(), 1L); + newHistory.setEncoding(currentHistory.getEncoding()); + newHistory.setResource(currentHistory.getResource()); + myResourceHistoryTableDao.save(newHistory); }); - mySystemDao.performReindexingPass(1000); - mySystemDao.performReindexingPass(1000); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); runInTransaction(() -> { Optional tableOpt = myResourceTableDao.findById(id1.getIdPartAsLong()); @@ -185,6 +201,48 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { } + @Test + public void testMissingVersionsAreReindexed() { + myDaoConfig.setSchedulingDisabled(true); + + Patient pt1 = new Patient(); + pt1.setActive(true); + pt1.addName().setFamily("FAM"); + IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + + runInTransaction(() -> { + assertThat(myResourceIndexedSearchParamTokenDao.countForResourceId(id1.getIdPartAsLong()), greaterThan(0)); + Optional tableOpt = myResourceTableDao.findById(id1.getIdPartAsLong()); + assertTrue(tableOpt.isPresent()); + assertEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXED, tableOpt.get().getIndexStatus().longValue()); + }); + + /* + * This triggers a new version in the HFJ_RESOURCE table, but + * we do not create the corresponding entry in the HFJ_RES_VER + * table. + */ + runInTransaction(() -> { + Optional tableOpt = myResourceTableDao.findById(id1.getIdPartAsLong()); + assertTrue(tableOpt.isPresent()); + ResourceTable table = tableOpt.get(); + table.setIndexStatus(null); + table.setDeleted(new Date()); + myResourceTableDao.saveAndFlush(table); + }); + + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + + runInTransaction(() -> { + Optional tableOpt = myResourceTableDao.findById(id1.getIdPartAsLong()); + assertTrue(tableOpt.isPresent()); + assertEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXED, tableOpt.get().getIndexStatus().longValue()); + assertThat(myResourceIndexedSearchParamTokenDao.countForResourceId(id1.getIdPartAsLong()), not(greaterThan(0))); + }); + + + } @Test public void testCantSearchForDeletedResourceByLanguageOrTag() { @@ -3014,17 +3072,17 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { Encounter e1 = new Encounter(); e1.addIdentifier().setSystem("foo").setValue(methodName); - e1.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); + e1.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("min").setValue(4.0 * 24 * 60); IIdType id1 = myEncounterDao.create(e1, mySrd).getId().toUnqualifiedVersionless(); Encounter e3 = new Encounter(); e3.addIdentifier().setSystem("foo").setValue(methodName); - e3.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("year").setValue(3.0); + e3.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("year").setValue(3.0); IIdType id3 = myEncounterDao.create(e3, mySrd).getId().toUnqualifiedVersionless(); Encounter e2 = new Encounter(); e2.addIdentifier().setSystem("foo").setValue(methodName); - e2.getLength().setSystem(BaseHapiFhirDao.UCUM_NS).setCode("year").setValue(2.0); + e2.getLength().setSystem(SearchParamConstants.UCUM_NS).setCode("year").setValue(2.0); IIdType id2 = myEncounterDao.create(e2, mySrd).getId().toUnqualifiedVersionless(); SearchParameterMap pm; 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 bd063d48975..34a042446d0 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 @@ -2,9 +2,12 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.SearchBuilder; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; -import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.util.JpaConstants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.DateParam; @@ -19,6 +22,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; @@ -44,15 +48,17 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { @After public void after() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); myDaoConfig.setUniqueIndexesCheckedBeforeSave(new DaoConfig().isUniqueIndexesCheckedBeforeSave()); myDaoConfig.setSchedulingDisabled(new DaoConfig().isSchedulingDisabled()); + myDaoConfig.setUniqueIndexesEnabled(new DaoConfig().isUniqueIndexesEnabled()); } @Before public void before() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); + myModelConfig.setDefaultSearchParamsCanBeOverridden(true); myDaoConfig.setSchedulingDisabled(true); + myDaoConfig.setUniqueIndexesEnabled(true); SearchBuilder.resetLastHandlerMechanismForUnitTest(); } @@ -87,7 +93,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setExpression("Patient") .setDefinition("SearchParameter/patient-birthdate"); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); @@ -130,7 +136,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setExpression("Coverage") .setDefinition("/SearchParameter/coverage-identifier"); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); mySearchParamRegsitry.forceRefresh(); @@ -159,7 +165,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setExpression("Observation") .setDefinition("/SearchParameter/observation-subject"); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); mySearchParamRegsitry.forceRefresh(); @@ -188,7 +194,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setExpression("Patient") .setDefinition("/SearchParameter/patient-identifier"); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); mySearchParamRegsitry.forceRefresh(); @@ -217,7 +223,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setExpression("Patient") .setDefinition("/SearchParameter/patient-identifier"); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); mySearchParamRegsitry.forceRefresh(); @@ -254,7 +260,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setExpression("Patient") .setDefinition("SearchParameter/patient-organization"); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); @@ -306,7 +312,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setExpression("Observation") .setDefinition("SearchParameter/obs-code"); sp.addExtension() - .setUrl(JpaConstants.EXT_SP_UNIQUE) + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); @@ -419,6 +425,10 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { } + @Autowired + private ISearchParamRegistry mySearchParamRegistry; + + @Test public void testDuplicateUniqueValuesAreReIndexed() { myDaoConfig.setSchedulingDisabled(true); @@ -449,9 +459,13 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { createUniqueObservationSubjectDateCode(); - mySystemDao.markAllResourcesForReindexing(); - mySystemDao.performReindexingPass(1000); - mySystemDao.performReindexingPass(1000); + List uniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams("Observation"); + assertEquals(1, uniqueSearchParams.size()); + assertEquals(3, uniqueSearchParams.get(0).getComponents().size()); + + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); assertEquals(uniques.toString(), 1, uniques.size()); @@ -462,9 +476,9 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { assertEquals(1, mySearchParamRegsitry.getActiveUniqueSearchParams("Observation").size()); - assertEquals(7, mySystemDao.markAllResourcesForReindexing()); - mySystemDao.performReindexingPass(1000); - mySystemDao.performReindexingPass(1000); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); assertEquals(uniques.toString(), 1, uniques.size()); @@ -557,10 +571,16 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { createUniqueIndexCoverageBeneficiary(); - mySystemDao.markAllResourcesForReindexing(); - mySystemDao.performReindexingPass(1000); + 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()); @@ -1119,8 +1139,9 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { pt2.setActive(false); myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); - mySystemDao.markAllResourcesForReindexing(); - mySystemDao.performReindexingPass(1000); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); assertEquals(uniques.toString(), 1, uniques.size()); @@ -1129,8 +1150,9 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { myResourceIndexedCompositeStringUniqueDao.deleteAll(); - mySystemDao.markAllResourcesForReindexing(); - mySystemDao.performReindexingPass(1000); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + myResourceReindexingSvc.forceReindexingPass(); uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); assertEquals(uniques.toString(), 1, uniques.size()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTagSnapshotTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTagSnapshotTest.java index 9f727ea1aa4..1d3f9e7f381 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTagSnapshotTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTagSnapshotTest.java @@ -10,7 +10,7 @@ import org.junit.AfterClass; import org.junit.Test; import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; public class FhirResourceDaoR4UpdateTagSnapshotTest extends BaseJpaR4Test { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java index 728a75f6950..aa6008ccc73 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java @@ -1,32 +1,36 @@ package ca.uhn.fhir.jpa.dao.r4; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; - -import java.util.*; - -import net.ttddyy.dsproxy.QueryCountHolder; -import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.*; -import org.mockito.ArgumentCaptor; - import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.rest.server.exceptions.*; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.util.TestUtil; +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.After; +import org.junit.AfterClass; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.test.context.TestPropertySource; +import java.util.*; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + @TestPropertySource(properties = { "scheduling_disabled=true" }) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4Test.java new file mode 100644 index 00000000000..9f805e83651 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4Test.java @@ -0,0 +1,80 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.SearchParameter; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class FhirResourceDaoSearchParameterR4Test { + + private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoSearchParameterR4Test.class); + private FhirContext myCtx; + private FhirResourceDaoSearchParameterR4 myDao; + + @Before + public void before() { + myCtx = FhirContext.forR4(); + myDao = new FhirResourceDaoSearchParameterR4(); + myDao.setContext(myCtx); + myDao.setConfig(new DaoConfig()); + } + + @Test + public void testValidateAllBuiltInSearchParams() { + + for (String nextResource : myCtx.getResourceNames()) { + RuntimeResourceDefinition nextResDef = myCtx.getResourceDefinition(nextResource); + for (RuntimeSearchParam nextp : nextResDef.getSearchParams()) { + if (nextp.getName().equals("_id")) { + continue; + } + if (nextp.getName().equals("_language")) { + continue; + } + if (isBlank(nextp.getPath())) { + continue; + } + + SearchParameter nextSearchParameter = new SearchParameter(); + nextSearchParameter.setExpression(nextp.getPath()); + nextSearchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); + nextSearchParameter.setType(Enumerations.SearchParamType.fromCode(nextp.getParamType().getCode())); + nextp.getBase().forEach(t -> nextSearchParameter.addBase(t)); + + ourLog.info("Validating {}.{}", nextResource, nextp.getName()); + myDao.validateResourceForStorage(nextSearchParameter, null); + } + } + + + } + + + @Test + public void testValidateInvalidExpression() { + SearchParameter nextSearchParameter = new SearchParameter(); + nextSearchParameter.setExpression("Patient////"); + nextSearchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE); + nextSearchParameter.setType(Enumerations.SearchParamType.STRING); + nextSearchParameter.addBase("Patient"); + + try { + myDao.validateResourceForStorage(nextSearchParameter, null); + fail(); + } catch (UnprocessableEntityException e) { + assertEquals("Invalid SearchParameter.expression value \"Patient////\": Error at 1, 1: Premature ExpressionNode termination at unexpected token \"////\"", e.getMessage()); + } + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java index f74222a0406..68886499c8f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java @@ -13,7 +13,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.util.TestUtil; 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 6e106dcfea2..1c5037f1c92 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 @@ -2,8 +2,8 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.primitive.IdDt; @@ -11,6 +11,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; @@ -36,10 +37,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.*; @@ -51,11 +49,6 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoR4Test.class); - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - @After public void after() { myDaoConfig.setAllowInlineMatchUrlReferences(false); @@ -176,6 +169,69 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { return null; } + @Test + public void testTransactionReSavesPreviouslyDeletedResources() { + + { + Bundle input = new Bundle(); + input.setType(BundleType.TRANSACTION); + + Patient pt = new Patient(); + pt.setId("pt"); + pt.setActive(true); + input + .addEntry() + .setResource(pt) + .getRequest() + .setUrl("Patient/pt") + .setMethod(HTTPVerb.PUT); + + Observation obs = new Observation(); + obs.setId("obs"); + obs.getSubject().setReference("Patient/pt"); + input + .addEntry() + .setResource(obs) + .getRequest() + .setUrl("Observation/obs") + .setMethod(HTTPVerb.PUT); + + mySystemDao.transaction(null, input); + } + + myObservationDao.delete(new IdType("Observation/obs")); + myPatientDao.delete(new IdType("Patient/pt")); + + { + Bundle input = new Bundle(); + input.setType(BundleType.TRANSACTION); + + Patient pt = new Patient(); + pt.setId("pt"); + pt.setActive(true); + input + .addEntry() + .setResource(pt) + .getRequest() + .setUrl("Patient/pt") + .setMethod(HTTPVerb.PUT); + + Observation obs = new Observation(); + obs.setId("obs"); + obs.getSubject().setReference("Patient/pt"); + input + .addEntry() + .setResource(obs) + .getRequest() + .setUrl("Observation/obs") + .setMethod(HTTPVerb.PUT); + + mySystemDao.transaction(null, input); + } + + myPatientDao.read(new IdType("Patient/pt")); + } + @Test public void testResourceCounts() { Patient p = new Patient(); @@ -461,82 +517,178 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { } @Test - public void testReindexing() { + public void testReindexing() throws InterruptedException { Patient p = new Patient(); p.addName().setFamily("family"); final IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualified(); + sleepUntilTimeChanges(); + ValueSet vs = new ValueSet(); vs.setUrl("http://foo"); myValueSetDao.create(vs, mySrd); - ResourceTable entity = new TransactionTemplate(myTxManager).execute(new TransactionCallback() { - @Override - public ResourceTable doInTransaction(TransactionStatus theStatus) { - return myEntityManager.find(ResourceTable.class, id.getIdPartAsLong()); - } - }); + sleepUntilTimeChanges(); + + ResourceTable entity = new TransactionTemplate(myTxManager).execute(t -> myEntityManager.find(ResourceTable.class, id.getIdPartAsLong())); assertEquals(Long.valueOf(1), entity.getIndexStatus()); - mySystemDao.markAllResourcesForReindexing(); + Long jobId = myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); - entity = new TransactionTemplate(myTxManager).execute(new TransactionCallback() { - @Override - public ResourceTable doInTransaction(TransactionStatus theStatus) { - return myEntityManager.find(ResourceTable.class, id.getIdPartAsLong()); - } - }); - assertEquals(null, entity.getIndexStatus()); - - mySystemDao.performReindexingPass(null); - - entity = new TransactionTemplate(myTxManager).execute(new TransactionCallback() { - @Override - public ResourceTable doInTransaction(TransactionStatus theStatus) { - return myEntityManager.find(ResourceTable.class, id.getIdPartAsLong()); - } - }); + entity = new TransactionTemplate(myTxManager).execute(t -> myEntityManager.find(ResourceTable.class, id.getIdPartAsLong())); assertEquals(Long.valueOf(1), entity.getIndexStatus()); // Just make sure this doesn't cause a choke - mySystemDao.performReindexingPass(100000); + 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); template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - template.execute(new TransactionCallback() { - @Override - public ResourceTable doInTransaction(TransactionStatus theStatus) { - ResourceHistoryTable resourceHistoryTable = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), id.getVersionIdPartAsLong()); - resourceHistoryTable.setEncoding(ResourceEncodingEnum.JSON); - try { - resourceHistoryTable.setResource("{\"resourceType\":\"FOO\"}".getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new Error(e); - } - myResourceHistoryTableDao.save(resourceHistoryTable); - - ResourceTable table = myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new); - table.setIndexStatus(null); - myResourceTableDao.save(table); - - return null; + template.execute((TransactionCallback) t -> { + ResourceHistoryTable resourceHistoryTable = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), id.getVersionIdPartAsLong()); + resourceHistoryTable.setEncoding(ResourceEncodingEnum.JSON); + try { + resourceHistoryTable.setResource("{\"resourceType\":\"FOO\"}".getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new Error(e); } + myResourceHistoryTableDao.save(resourceHistoryTable); + + ResourceTable table = myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new); + table.setIndexStatus(null); + myResourceTableDao.save(table); + + return null; }); - mySystemDao.performReindexingPass(null); + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); - entity = new TransactionTemplate(myTxManager).execute(new TransactionCallback() { - @Override - public ResourceTable doInTransaction(TransactionStatus theStatus) { - return myEntityManager.find(ResourceTable.class, id.getIdPartAsLong()); - } - }); + entity = new TransactionTemplate(myTxManager).execute(theStatus -> myEntityManager.find(ResourceTable.class, id.getIdPartAsLong())); assertEquals(Long.valueOf(2), entity.getIndexStatus()); } + @Test + public void testReindexingCurrentVersionDeleted() { + Patient p = new Patient(); + p.addName().setFamily("family1"); + final IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + + p = new Patient(); + p.setId(id); + p.addName().setFamily("family1"); + p.addName().setFamily("family2"); + myPatientDao.update(p); + + p = new Patient(); + p.setId(id); + p.addName().setFamily("family1"); + p.addName().setFamily("family2"); + p.addName().setFamily("family3"); + myPatientDao.update(p); + + SearchParameterMap searchParamMap = new SearchParameterMap(); + searchParamMap.setLoadSynchronous(true); + searchParamMap.add(Patient.SP_FAMILY, new StringParam("family2")); + assertEquals(1, myPatientDao.search(searchParamMap).size().intValue()); + + runInTransaction(()->{ + ResourceHistoryTable historyEntry = myResourceHistoryTableDao.findForIdAndVersion(id.getIdPartAsLong(), 3); + assertNotNull(historyEntry); + myResourceHistoryTableDao.delete(historyEntry); + }); + + Long jobId = myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + + searchParamMap = new SearchParameterMap(); + searchParamMap.setLoadSynchronous(true); + searchParamMap.add(Patient.SP_FAMILY, new StringParam("family2")); + IBundleProvider search = myPatientDao.search(searchParamMap); + assertEquals(1, search.size().intValue()); + p = (Patient) search.getResources(0, 1).get(0); + assertEquals("3", p.getIdElement().getVersionIdPart()); + } + + + @Test + public void testReindexingSingleStringHashValueIsDeleted() { + Patient p = new Patient(); + p.addName().setFamily("family1"); + final IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + + SearchParameterMap searchParamMap = new SearchParameterMap(); + searchParamMap.setLoadSynchronous(true); + searchParamMap.add(Patient.SP_FAMILY, new StringParam("family1")); + assertEquals(1, myPatientDao.search(searchParamMap).size().intValue()); + + runInTransaction(()->{ + myEntityManager + .createQuery("UPDATE ResourceIndexedSearchParamString s SET s.myHashNormalizedPrefix = null") + .executeUpdate(); + }); + + assertEquals(0, myPatientDao.search(searchParamMap).size().intValue()); + + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + + assertEquals(1, myPatientDao.search(searchParamMap).size().intValue()); + } + + @Test + public void testReindexingSingleStringHashIdentityValueIsDeleted() { + Patient p = new Patient(); + p.addName().setFamily("family1"); + final IIdType id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + + SearchParameterMap searchParamMap = new SearchParameterMap(); + searchParamMap.setLoadSynchronous(true); + searchParamMap.add(Patient.SP_FAMILY, new StringParam("family1")); + assertEquals(1, myPatientDao.search(searchParamMap).size().intValue()); + + runInTransaction(()->{ + Long i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myHashIdentity IS null", Long.class) + .getSingleResult(); + assertEquals(0L, i.longValue()); + + myEntityManager + .createQuery("UPDATE ResourceIndexedSearchParamString s SET s.myHashIdentity = null") + .executeUpdate(); + + i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myHashIdentity IS null", Long.class) + .getSingleResult(); + assertThat(i, greaterThan(1L)); + + }); + + myResourceReindexingSvc.markAllResourcesForReindexing(); + myResourceReindexingSvc.forceReindexingPass(); + + runInTransaction(()->{ + Long i = myEntityManager + .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myHashIdentity IS null", Long.class) + .getSingleResult(); + assertEquals(0L, i.longValue()); + }); + } + @Test public void testSystemMetaOperation() { @@ -783,6 +935,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(); @@ -3055,6 +3282,44 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { assertEquals(1, found.size().intValue()); } + @Test + public void testTransactionWithRelativeOidIds() { + Bundle res = new Bundle(); + res.setType(BundleType.TRANSACTION); + + Patient p1 = new Patient(); + p1.setId("urn:oid:0.1.2.3"); + p1.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds01"); + res.addEntry().setResource(p1).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient"); + + Observation o1 = new Observation(); + o1.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds02"); + o1.setSubject(new Reference("urn:oid:0.1.2.3")); + res.addEntry().setResource(o1).getRequest().setMethod(HTTPVerb.POST).setUrl("Observation"); + + Observation o2 = new Observation(); + o2.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds03"); + o2.setSubject(new Reference("urn:oid:0.1.2.3")); + res.addEntry().setResource(o2).getRequest().setMethod(HTTPVerb.POST).setUrl("Observation"); + + Bundle resp = mySystemDao.transaction(mySrd, res); + + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp)); + + assertEquals(BundleType.TRANSACTIONRESPONSE, resp.getTypeElement().getValue()); + assertEquals(3, resp.getEntry().size()); + + assertTrue(resp.getEntry().get(0).getResponse().getLocation(), new IdType(resp.getEntry().get(0).getResponse().getLocation()).getIdPart().matches("^[0-9]+$")); + assertTrue(resp.getEntry().get(1).getResponse().getLocation(), new IdType(resp.getEntry().get(1).getResponse().getLocation()).getIdPart().matches("^[0-9]+$")); + assertTrue(resp.getEntry().get(2).getResponse().getLocation(), new IdType(resp.getEntry().get(2).getResponse().getLocation()).getIdPart().matches("^[0-9]+$")); + + o1 = myObservationDao.read(new IdType(resp.getEntry().get(1).getResponse().getLocation()), mySrd); + o2 = myObservationDao.read(new IdType(resp.getEntry().get(2).getResponse().getLocation()), mySrd); + assertThat(o1.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart())); + assertThat(o2.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart())); + + } + // // // /** @@ -3157,44 +3422,6 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { // // } - @Test - public void testTransactionWithRelativeOidIds() { - Bundle res = new Bundle(); - res.setType(BundleType.TRANSACTION); - - Patient p1 = new Patient(); - p1.setId("urn:oid:0.1.2.3"); - p1.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds01"); - res.addEntry().setResource(p1).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient"); - - Observation o1 = new Observation(); - o1.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds02"); - o1.setSubject(new Reference("urn:oid:0.1.2.3")); - res.addEntry().setResource(o1).getRequest().setMethod(HTTPVerb.POST).setUrl("Observation"); - - Observation o2 = new Observation(); - o2.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds03"); - o2.setSubject(new Reference("urn:oid:0.1.2.3")); - res.addEntry().setResource(o2).getRequest().setMethod(HTTPVerb.POST).setUrl("Observation"); - - Bundle resp = mySystemDao.transaction(mySrd, res); - - ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp)); - - assertEquals(BundleType.TRANSACTIONRESPONSE, resp.getTypeElement().getValue()); - assertEquals(3, resp.getEntry().size()); - - assertTrue(resp.getEntry().get(0).getResponse().getLocation(), new IdType(resp.getEntry().get(0).getResponse().getLocation()).getIdPart().matches("^[0-9]+$")); - assertTrue(resp.getEntry().get(1).getResponse().getLocation(), new IdType(resp.getEntry().get(1).getResponse().getLocation()).getIdPart().matches("^[0-9]+$")); - assertTrue(resp.getEntry().get(2).getResponse().getLocation(), new IdType(resp.getEntry().get(2).getResponse().getLocation()).getIdPart().matches("^[0-9]+$")); - - o1 = myObservationDao.read(new IdType(resp.getEntry().get(1).getResponse().getLocation()), mySrd); - o2 = myObservationDao.read(new IdType(resp.getEntry().get(2).getResponse().getLocation()), mySrd); - assertThat(o1.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart())); - assertThat(o2.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart())); - - } - /** * This is not the correct way to do it, but we'll allow it to be lenient */ @@ -3407,4 +3634,9 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { } + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + } 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..66a932188fe 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 @@ -3,14 +3,11 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.FhirContext; 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.ISearchParamRegistry; -import ca.uhn.fhir.jpa.dao.PathAndRef; -import ca.uhn.fhir.jpa.entity.BaseResourceIndexedSearchParam; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamQuantity; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; @@ -22,10 +19,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 +79,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; + } }; } @@ -94,7 +98,7 @@ public class SearchParamExtractorR4Test { Observation obs = new Observation(); obs.addCategory().addCoding().setSystem("SYSTEM").setCode("CODE"); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new DaoConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); Set tokens = extractor.extractSearchParamTokens(new ResourceTable(), obs); assertEquals(1, tokens.size()); ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.iterator().next(); @@ -108,7 +112,7 @@ public class SearchParamExtractorR4Test { Encounter enc = new Encounter(); enc.addLocation().setLocation(new Reference("Location/123")); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new DaoConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam("Encounter", "location"); assertNotNull(param); List links = extractor.extractResourceLinks(enc, param); @@ -122,7 +126,7 @@ public class SearchParamExtractorR4Test { Consent consent = new Consent(); consent.setSource(new Reference().setReference("Consent/999")); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new DaoConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam("Consent", Consent.SP_SOURCE_REFERENCE); assertNotNull(param); List links = extractor.extractResourceLinks(consent, param); @@ -141,7 +145,7 @@ public class SearchParamExtractorR4Test { .setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code2"))) .setValue(new Quantity().setSystem("http://bar").setCode("code2").setValue(200)); - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new DaoConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new ModelConfig(), ourCtx, ourValidationSupport, mySearchParamRegistry); Set links = extractor.extractSearchParamQuantity(new ResourceTable(), o1); ourLog.info("Links:\n {}", links.stream().map(t -> t.toString()).collect(Collectors.joining("\n "))); assertEquals(4, links.size()); 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..91c8534ffdf --- /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.searchparam.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/SearchParameterMapTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SearchParameterMapTest.java index b6ce39433db..ea440839852 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SearchParameterMapTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SearchParameterMapTest.java @@ -1,14 +1,13 @@ package ca.uhn.fhir.jpa.provider; -import static java.util.Collections.addAll; import static org.junit.Assert.assertEquals; import org.junit.AfterClass; import org.junit.Test; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java index 99340a794c3..ae52fbdfcd3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java @@ -1,12 +1,25 @@ package ca.uhn.fhir.jpa.provider; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; - -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.dstu2.BaseJpaDstu2Test; +import ca.uhn.fhir.jpa.rp.dstu2.*; +import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; +import ca.uhn.fhir.model.dstu2.resource.*; +import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum; +import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; +import ca.uhn.fhir.model.primitive.DecimalDt; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; +import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -17,46 +30,32 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.IdType; import org.junit.AfterClass; import org.junit.Before; -import org.junit.Test; import org.junit.Ignore; +import org.junit.Test; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.dao.dstu2.BaseJpaDstu2Test; -import ca.uhn.fhir.jpa.rp.dstu2.*; -import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; -import ca.uhn.fhir.model.dstu2.resource.*; -import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum; -import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; -import ca.uhn.fhir.model.primitive.*; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; -import ca.uhn.fhir.util.TestUtil; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; public class SystemProviderDstu2Test extends BaseJpaDstu2Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SystemProviderDstu2Test.class); private static RestfulServer myRestServer; private static IGenericClient ourClient; private static FhirContext ourCtx; private static CloseableHttpClient ourHttpClient; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SystemProviderDstu2Test.class); private static Server ourServer; private static String ourServerBase; - @AfterClass - public static void afterClassClearContext() throws Exception { - ourServer.stop(); - TestUtil.clearAllStaticFieldsForUnitTest(); - } - - @Before public void beforeStartServer() throws Exception { if (myRestServer == null) { @@ -72,9 +71,23 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { OrganizationResourceProvider organizationRp = new OrganizationResourceProvider(); organizationRp.setDao(myOrganizationDao); + LocationResourceProvider locationRp = new LocationResourceProvider(); + locationRp.setDao(myLocationDao); + + BinaryResourceProvider binaryRp = new BinaryResourceProvider(); + binaryRp.setDao(myBinaryDao); + + DiagnosticReportResourceProvider diagnosticReportRp = new DiagnosticReportResourceProvider(); + diagnosticReportRp.setDao(myDiagnosticReportDao); + DiagnosticOrderResourceProvider diagnosticOrderRp = new DiagnosticOrderResourceProvider(); + diagnosticOrderRp.setDao(myDiagnosticOrderDao); + PractitionerResourceProvider practitionerRp = new PractitionerResourceProvider(); + practitionerRp.setDao(myPractitionerDao); + + RestfulServer restServer = new RestfulServer(ourCtx); restServer.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(10)); - restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp); + restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp, binaryRp, locationRp, diagnosticReportRp, diagnosticOrderRp, practitionerRp); restServer.setPlainProviders(mySystemProvider); @@ -157,10 +170,10 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { ourLog.info(response); assertThat(response, not(containsString("_format"))); assertEquals(200, http.getStatusLine().getStatusCode()); - + Bundle responseBundle = ourCtx.newXmlParser().parseResource(Bundle.class, response); assertEquals(BundleTypeEnum.SEARCH_RESULTS, responseBundle.getTypeElement().getValueAsEnum()); - + } finally { http.close(); } @@ -179,10 +192,10 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { } } - @Transactional(propagation=Propagation.NEVER) + @Transactional(propagation = Propagation.NEVER) @Test public void testSuggestKeywords() throws Exception { - + Patient patient = new Patient(); patient.addName().addFamily("testSuggest"); IIdType ptId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); @@ -197,21 +210,21 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { obs.getSubject().setReference(ptId); obs.getCode().setText("ZXCVBNM ASDFGHJKL QWERTYUIOPASDFGHJKL"); myObservationDao.update(obs, mySrd); - + HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + ptId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); CloseableHttpResponse http = ourHttpClient.execute(get); try { assertEquals(200, http.getStatusLine().getStatusCode()); String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(output); - + Parameters parameters = ourCtx.newXmlParser().parseResource(Parameters.class, output); assertEquals(2, parameters.getParameter().size()); assertEquals("keyword", parameters.getParameter().get(0).getPart().get(0).getName()); assertEquals(new StringDt("ZXCVBNM"), parameters.getParameter().get(0).getPart().get(0).getValue()); assertEquals("score", parameters.getParameter().get(0).getPart().get(1).getName()); assertEquals(new DecimalDt("1.0"), parameters.getParameter().get(0).getPart().get(1).getValue()); - + } finally { http.close(); } @@ -227,7 +240,7 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { obs.getSubject().setReference(ptId); obs.getCode().setText("ZXCVBNM ASDFGHJKL QWERTYUIOPASDFGHJKL"); myObservationDao.create(obs, mySrd); - + HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords"); CloseableHttpResponse http = ourHttpClient.execute(get); try { @@ -238,7 +251,7 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { } finally { http.close(); } - + get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + ptId.getIdPart() + "/$everything"); http = ourHttpClient.execute(get); try { @@ -269,6 +282,44 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { assertEquals("get-resource-counts", op.getCode()); } + @Test + public void testTransactionReSavesPreviouslyDeletedResources() throws IOException { + + for (int i = 0; i < 10; i++) { + ourLog.info("** Beginning pass {}", i); + + Bundle input = myFhirCtx.newJsonParser().parseResource(Bundle.class, IOUtils.toString(getClass().getResourceAsStream("/dstu2/createdeletebundle.json"), Charsets.UTF_8)); + ourClient.transaction().withBundle(input).execute(); + + myPatientDao.read(new IdType("Patient/Patient1063259")); + + deleteAllOfType("Binary"); + deleteAllOfType("Location"); + deleteAllOfType("DiagnosticReport"); + deleteAllOfType("Observation"); + deleteAllOfType("DiagnosticOrder"); + deleteAllOfType("Practitioner"); + deleteAllOfType("Patient"); + deleteAllOfType("Organization"); + + try { + myPatientDao.read(new IdType("Patient/Patient1063259")); + fail(); + } catch (ResourceGoneException e) { + // good + } + + } + + } + + private void deleteAllOfType(String theType) { + BundleUtil.toListOfResources(myFhirCtx, ourClient.search().forResource(theType).execute()) + .forEach(t -> { + ourClient.delete().resourceById(t.getIdElement()).execute(); + }); + } + @Test public void testTransactionFromBundle() throws Exception { InputStream bundleRes = SystemProviderDstu2Test.class.getResourceAsStream("/transaction_link_patient_eve.xml"); @@ -372,20 +423,20 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { @Test public void testTransactionSearch() throws Exception { - for (int i = 0; i < 20; i ++) { + for (int i = 0; i < 20; i++) { Patient p = new Patient(); p.addName().addFamily("PATIENT_" + i); myPatientDao.create(p, mySrd); } - + Bundle req = new Bundle(); req.setType(BundleTypeEnum.TRANSACTION); req.addEntry().getRequest().setMethod(HTTPVerbEnum.GET).setUrl("Patient?"); Bundle resp = ourClient.transaction().withBundle(req).execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp)); - + assertEquals(1, resp.getEntry().size()); - Bundle respSub = (Bundle)resp.getEntry().get(0).getResource(); + Bundle respSub = (Bundle) resp.getEntry().get(0).getResource(); assertEquals("self", respSub.getLink().get(0).getRelation()); assertEquals(ourServerBase + "/Patient", respSub.getLink().get(0).getUrl()); assertEquals("next", respSub.getLink().get(1).getRelation()); @@ -396,20 +447,20 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { @Test public void testTransactionCount() throws Exception { - for (int i = 0; i < 20; i ++) { + for (int i = 0; i < 20; i++) { Patient p = new Patient(); p.addName().addFamily("PATIENT_" + i); myPatientDao.create(p, mySrd); } - + Bundle req = new Bundle(); req.setType(BundleTypeEnum.TRANSACTION); req.addEntry().getRequest().setMethod(HTTPVerbEnum.GET).setUrl("Patient?_summary=count"); Bundle resp = ourClient.transaction().withBundle(req).execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp)); - + assertEquals(1, resp.getEntry().size()); - Bundle respSub = (Bundle)resp.getEntry().get(0).getResource(); + Bundle respSub = (Bundle) resp.getEntry().get(0).getResource(); assertEquals(20, respSub.getTotal().intValue()); assertEquals(0, respSub.getEntry().size()); } @@ -423,9 +474,16 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { ourLog.info(output); assertEquals(200, http.getStatusLine().getStatusCode()); } finally { - IOUtils.closeQuietly(http);; + IOUtils.closeQuietly(http); + ; } } + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java index 32c1648a882..62e2534f45d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java @@ -3,10 +3,10 @@ package ca.uhn.fhir.jpa.provider.dstu3; import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; -import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu3; import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor; import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor; import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderCustomSearchParamDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderCustomSearchParamDstu3Test.java index 1730f0d8523..1297e57e86e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderCustomSearchParamDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderCustomSearchParamDstu3Test.java @@ -7,6 +7,9 @@ import static org.junit.Assert.*; import java.io.IOException; import java.util.*; +import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -22,7 +25,7 @@ import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; @@ -39,7 +42,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv public void after() throws Exception { super.after(); - myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); } @Override @@ -52,7 +55,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv public void beforeResetConfig() { super.beforeResetConfig(); - myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); mySearchParamRegsitry.forceRefresh(); } @@ -89,7 +92,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv @Test public void testConformanceOverrideAllowed() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); + myModelConfig.setDefaultSearchParamsCanBeOverridden(true); CapabilityStatement conformance = ourClient .fetchConformance() @@ -159,7 +162,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv @Test public void testConformanceOverrideNotAllowed() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(false); + myModelConfig.setDefaultSearchParamsCanBeOverridden(false); CapabilityStatement conformance = ourClient .fetchConformance() @@ -236,11 +239,11 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv fooSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(fooSp, mySrd); - res = myResourceTableDao.findById(patId.getIdPartAsLong()).orElseThrow(IllegalStateException::new); - assertEquals(null, res.getIndexStatus()); - res = myResourceTableDao.findById(obsId.getIdPartAsLong()).orElseThrow(IllegalStateException::new); - assertEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXED, res.getIndexStatus().longValue()); - + runInTransaction(()->{ + List allJobs = myResourceReindexJobDao.findAll(); + assertEquals(1, allJobs.size()); + assertEquals("Patient", allJobs.get(0).getResourceType()); + }); } @Test diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java index 142a4963378..bf963b4e79e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java @@ -75,11 +75,6 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderDstu3Test.class); private SearchCoordinatorSvcImpl mySearchCoordinatorSvcRaw; - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - @Override @After public void after() throws Exception { @@ -240,6 +235,59 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { } } + @Test + public void testSearchChainedReference() { + + Patient p = new Patient(); + p.addName().setFamily("SMITH"); + IIdType pid = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); + + QuestionnaireResponse qr = new QuestionnaireResponse(); + qr.getSubject().setReference(pid.getValue()); + ourClient.create().resource(qr).execute(); + + Subscription subs = new Subscription(); + subs.setStatus(SubscriptionStatus.ACTIVE); + subs.getChannel().setType(SubscriptionChannelType.WEBSOCKET); + subs.setCriteria("Observation?"); + IIdType id = ourClient.create().resource(subs).execute().getId().toUnqualifiedVersionless(); + + // Unqualified (doesn't work because QuestionnaireRespone.subject is a Refercence(Any)) + try { + ourClient + .search() + .forResource(QuestionnaireResponse.class) + .where(QuestionnaireResponse.SUBJECT.hasChainedProperty(Patient.FAMILY.matches().value("SMITH"))) + .returnBundle(Bundle.class) + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: Unable to perform search for unqualified chain 'subject' as this SearchParameter does not declare any target types. Add a qualifier of the form 'subject:[ResourceType]' to perform this search.", e.getMessage()); + } + + // Qualified + Bundle resp = ourClient + .search() + .forResource(QuestionnaireResponse.class) + .where(QuestionnaireResponse.SUBJECT.hasChainedProperty("Patient", Patient.FAMILY.matches().value("SMITH"))) + .returnBundle(Bundle.class) + .execute(); + assertEquals(1, resp.getEntry().size()); + + // Qualified With an invalid name + try { + ourClient + .search() + .forResource(QuestionnaireResponse.class) + .where(QuestionnaireResponse.SUBJECT.hasChainedProperty("FOO", Patient.FAMILY.matches().value("SMITH"))) + .returnBundle(Bundle.class) + .execute(); + } catch (InvalidRequestException e) { + assertEquals("HTTP 400 Bad Request: Invalid resource type: FOO", e.getMessage()); + } + + } + @Test public void testCodeSearch() { Subscription subs = new Subscription(); @@ -1588,6 +1636,25 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { assertEquals(77, ids.size()); } + @Test + public void testEverythingWithOnlyPatient() { + Patient p = new Patient(); + p.setActive(true); + IIdType id = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); + + myFhirCtx.getRestfulClientFactory().setSocketTimeout(300 * 1000); + + Bundle response = ourClient + .operation() + .onInstance(id) + .named("everything") + .withNoParameters(Parameters.class) + .returnResourceType(Bundle.class) + .execute(); + + assertEquals(1, response.getEntry().size()); + } + // private void delete(String theResourceType, String theParamName, String theParamValue) { // Bundle resources; // do { @@ -1613,25 +1680,6 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { // } // } - @Test - public void testEverythingWithOnlyPatient() { - Patient p = new Patient(); - p.setActive(true); - IIdType id = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); - - myFhirCtx.getRestfulClientFactory().setSocketTimeout(300 * 1000); - - Bundle response = ourClient - .operation() - .onInstance(id) - .named("everything") - .withNoParameters(Parameters.class) - .returnResourceType(Bundle.class) - .execute(); - - assertEquals(1, response.getEntry().size()); - } - @SuppressWarnings("unused") @Test public void testFullTextSearch() throws Exception { @@ -4293,4 +4341,9 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { return new InstantDt(theDate).getValueAsString(); } + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java index 1a7351110ff..3b0b4a91101 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java @@ -3,7 +3,7 @@ package ca.uhn.fhir.jpa.provider.dstu3; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TerminologyTest; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java index edd8de32d79..8fe97c13e6d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java @@ -3,7 +3,7 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig; import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; -import ca.uhn.fhir.jpa.dao.r4.SearchParamRegistryR4; +import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryR4; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java index 14659fca509..9e82d7f39f5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java @@ -2,15 +2,14 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; -import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; -import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.ExpungeOptions; +import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.TestUtil; -import org.hamcrest.Matchers; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Observation; @@ -19,7 +18,8 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; @@ -168,17 +168,17 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { myPatientDao.update(p).getId(); myPatientDao.delete(new IdType("Patient/TEST")); - runInTransaction(()-> assertThat(myResourceTableDao.findAll(), not(empty()))); - runInTransaction(()-> assertThat(myResourceHistoryTableDao.findAll(), not(empty()))); - runInTransaction(()-> assertThat(myForcedIdDao.findAll(), not(empty()))); + runInTransaction(() -> assertThat(myResourceTableDao.findAll(), not(empty()))); + runInTransaction(() -> assertThat(myResourceHistoryTableDao.findAll(), not(empty()))); + runInTransaction(() -> assertThat(myForcedIdDao.findAll(), not(empty()))); myPatientDao.expunge(new ExpungeOptions() .setExpungeDeletedResources(true) .setExpungeOldVersions(true)); - runInTransaction(()-> assertThat(myResourceTableDao.findAll(), empty())); - runInTransaction(()-> assertThat(myResourceHistoryTableDao.findAll(), empty())); - runInTransaction(()-> assertThat(myForcedIdDao.findAll(), empty())); + runInTransaction(() -> assertThat(myResourceTableDao.findAll(), empty())); + runInTransaction(() -> assertThat(myResourceHistoryTableDao.findAll(), empty())); + runInTransaction(() -> assertThat(myForcedIdDao.findAll(), empty())); } @@ -332,6 +332,61 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { assertGone(myDeletedObservationId); } + @Test + public void testExpungeEverythingWhereResourceInSearchResults() { + createStandardPatients(); + + IBundleProvider search = myPatientDao.search(new SearchParameterMap()); + assertEquals(2, search.size().intValue()); + search.getResources(0, 2); + + runInTransaction(() -> { + assertEquals(2, mySearchResultDao.count()); + }); + + mySystemDao.expunge(new ExpungeOptions() + .setExpungeEverything(true)); + + // Everything deleted + assertExpunged(myOneVersionPatientId); + assertExpunged(myTwoVersionPatientId.withVersion("1")); + assertExpunged(myTwoVersionPatientId.withVersion("2")); + assertExpunged(myDeletedPatientId.withVersion("1")); + assertExpunged(myDeletedPatientId); + + // Everything deleted + assertExpunged(myOneVersionObservationId); + assertExpunged(myTwoVersionObservationId.withVersion("1")); + assertExpunged(myTwoVersionObservationId.withVersion("2")); + assertExpunged(myDeletedObservationId); + } + + @Test + public void testExpungeDeletedWhereResourceInSearchResults() { + createStandardPatients(); + + IBundleProvider search = myPatientDao.search(new SearchParameterMap()); + assertEquals(2, search.size().intValue()); + List resources = search.getResources(0, 2); + myPatientDao.delete(resources.get(0).getIdElement()); + + runInTransaction(() -> { + assertEquals(2, mySearchResultDao.count()); + }); + + + mySystemDao.expunge(new ExpungeOptions() + .setExpungeDeletedResources(true)); + + // Everything deleted + assertExpunged(myOneVersionPatientId); + assertStillThere(myTwoVersionPatientId.withVersion("1")); + assertStillThere(myTwoVersionPatientId.withVersion("2")); + assertExpunged(myDeletedPatientId.withVersion("1")); + assertExpunged(myDeletedPatientId); + + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java index 19e36cdb5a3..953aed5ef72 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java @@ -7,6 +7,9 @@ import static org.junit.Assert.*; import java.io.IOException; import java.util.*; +import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -22,7 +25,7 @@ import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; @@ -39,7 +42,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide public void after() throws Exception { super.after(); - myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); } @Override @@ -52,7 +55,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide public void beforeResetConfig() { super.beforeResetConfig(); - myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); + myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); mySearchParamRegsitry.forceRefresh(); } @@ -89,7 +92,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide @Test public void testConformanceOverrideAllowed() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(true); + myModelConfig.setDefaultSearchParamsCanBeOverridden(true); CapabilityStatement conformance = ourClient .fetchConformance() @@ -159,7 +162,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide @Test public void testConformanceOverrideNotAllowed() { - myDaoConfig.setDefaultSearchParamsCanBeOverridden(false); + myModelConfig.setDefaultSearchParamsCanBeOverridden(false); CapabilityStatement conformance = ourClient .fetchConformance() @@ -236,10 +239,11 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(fooSp, mySrd); - res = myResourceTableDao.findById(patId.getIdPartAsLong()).orElseThrow(IllegalStateException::new); - assertEquals(null, res.getIndexStatus()); - res = myResourceTableDao.findById(obsId.getIdPartAsLong()).orElseThrow(IllegalStateException::new); - assertEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXED, res.getIndexStatus().longValue()); + runInTransaction(()->{ + List allJobs = myResourceReindexJobDao.findAll(); + assertEquals(1, allJobs.size()); + assertEquals("Patient", allJobs.get(0).getResourceType()); + }); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 8c678a06312..495ed9f54b8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -17,13 +17,7 @@ import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.stringContainsInOrder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import java.io.BufferedReader; import java.io.IOException; @@ -41,6 +35,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -90,7 +85,7 @@ import com.google.common.collect.Lists; import ca.uhn.fhir.jpa.config.TestR4Config; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; import ca.uhn.fhir.jpa.util.JpaConstants; @@ -159,6 +154,50 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); } + + @Test + public void testSearchLinksWorkWithIncludes() { + for (int i = 0; i < 5; i++) { + + Organization o = new Organization(); + o.setId("O" + i); + o.setName("O" + i); + IIdType oid = ourClient.update().resource(o).execute().getId().toUnqualifiedVersionless(); + + Patient p = new Patient(); + p.setId("P" + i); + p.getManagingOrganization().setReference(oid.getValue()); + ourClient.update().resource(p).execute(); + + } + + Bundle output = ourClient + .search() + .forResource("Patient") + .include(IBaseResource.INCLUDE_ALL) + .count(3) + .returnBundle(Bundle.class) + .execute(); + + List ids = output.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList()); + ourLog.info("Ids: {}", ids); + assertEquals(6, output.getEntry().size()); + assertNotNull(output.getLink("next")); + + // Page 2 + output = ourClient + .loadPage() + .next(output) + .execute(); + + ids = output.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList()); + ourLog.info("Ids: {}", ids); + assertEquals(4, output.getEntry().size()); + assertNull(output.getLink("next")); + + } + + @Test public void testDeleteConditional() { @@ -1658,27 +1697,25 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { .returnResourceType(Bundle.class) .execute(); - TreeSet ids = new TreeSet<>(); + ArrayList ids = new ArrayList<>(); for (int i = 0; i < responseBundle.getEntry().size(); i++) { - for (BundleEntryComponent nextEntry : responseBundle.getEntry()) { - ids.add(nextEntry.getResource().getIdElement().getIdPart()); - } + BundleEntryComponent nextEntry = responseBundle.getEntry().get(i); + ids.add(nextEntry.getResource().getIdElement().getIdPart()); } BundleLinkComponent nextLink = responseBundle.getLink("next"); - ourLog.info("Have {} IDs with next link: ", ids.size(), nextLink); + ourLog.info("Have {} IDs with next link[{}] : {}", ids.size(), nextLink, ids); while (nextLink != null) { String nextUrl = nextLink.getUrl(); responseBundle = ourClient.fetchResourceFromUrl(Bundle.class, nextUrl); for (int i = 0; i < responseBundle.getEntry().size(); i++) { - for (BundleEntryComponent nextEntry : responseBundle.getEntry()) { - ids.add(nextEntry.getResource().getIdElement().getIdPart()); - } + BundleEntryComponent nextEntry = responseBundle.getEntry().get(i); + ids.add(nextEntry.getResource().getIdElement().getIdPart()); } nextLink = responseBundle.getLink("next"); - ourLog.info("Have {} IDs with next link: ", ids.size(), nextLink); + ourLog.info("Have {} IDs with next link[{}] : {}", ids.size(), nextLink, ids); } assertThat(ids, hasItem(id.getIdPart())); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java index 7942a2dfe85..4b0773ae70f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetTest.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; 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..80d06765fa5 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 @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; import ca.uhn.fhir.rest.gclient.IClientExecutable; import ca.uhn.fhir.rest.gclient.IQuery; @@ -32,7 +33,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 +96,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/provider/r4/SystemProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java index 0a74eb50b8b..137d05f9492 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java @@ -3,22 +3,25 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; -import ca.uhn.fhir.jpa.rp.r4.ObservationResourceProvider; -import ca.uhn.fhir.jpa.rp.r4.OrganizationResourceProvider; -import ca.uhn.fhir.jpa.rp.r4.PatientResourceProvider; +import ca.uhn.fhir.jpa.rp.r4.*; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; +import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; +import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.ResultSeverityEnum; +import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.client.methods.CloseableHttpResponse; @@ -42,6 +45,7 @@ import org.junit.*; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; @@ -88,8 +92,21 @@ public class SystemProviderR4Test extends BaseJpaR4Test { OrganizationResourceProvider organizationRp = new OrganizationResourceProvider(); organizationRp.setDao(myOrganizationDao); + LocationResourceProvider locationRp = new LocationResourceProvider(); + locationRp.setDao(myLocationDao); + + BinaryResourceProvider binaryRp = new BinaryResourceProvider(); + binaryRp.setDao(myBinaryDao); + + DiagnosticReportResourceProvider diagnosticReportRp = new DiagnosticReportResourceProvider(); + diagnosticReportRp.setDao(myDiagnosticReportDao); + ServiceRequestResourceProvider diagnosticOrderRp = new ServiceRequestResourceProvider(); + diagnosticOrderRp.setDao(myServiceRequestDao); + PractitionerResourceProvider practitionerRp = new PractitionerResourceProvider(); + practitionerRp.setDao(myPractitionerDao); + RestfulServer restServer = new RestfulServer(ourCtx); - restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp); + restServer.setResourceProviders(patientRp, questionnaireRp, observationRp, organizationRp, locationRp, binaryRp, diagnosticReportRp, diagnosticOrderRp, practitionerRp); restServer.setPlainProviders(mySystemProvider); @@ -385,6 +402,55 @@ public class SystemProviderR4Test extends BaseJpaR4Test { assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus()); } + + @Test + public void testTransactionReSavesPreviouslyDeletedResources() throws IOException { + + for (int i = 0; i < 10; i++) { + ourLog.info("** Beginning pass {}", i); + + Bundle input = myFhirCtx.newJsonParser().parseResource(Bundle.class, IOUtils.toString(getClass().getResourceAsStream("/r4/createdeletebundle.json"), Charsets.UTF_8)); + ourClient.transaction().withBundle(input).execute(); + + myPatientDao.read(new IdType("Patient/Patient1063259")); + + + SearchParameterMap params = new SearchParameterMap(); + params.add("subject", new ReferenceParam("Patient1063259")); + params.setLoadSynchronous(true); + IBundleProvider result = myDiagnosticReportDao.search(params); + assertEquals(1, result.size().intValue()); + + deleteAllOfType("Binary"); + deleteAllOfType("Location"); + deleteAllOfType("DiagnosticReport"); + deleteAllOfType("Observation"); + deleteAllOfType("ServiceRequest"); + deleteAllOfType("Practitioner"); + deleteAllOfType("Patient"); + deleteAllOfType("Organization"); + + try { + myPatientDao.read(new IdType("Patient/Patient1063259")); + fail(); + } catch (ResourceGoneException e) { + // good + } + + result = myDiagnosticReportDao.search(params); + assertEquals(0, result.size().intValue()); + + } + + } + + private void deleteAllOfType(String theType) { + BundleUtil.toListOfResources(myFhirCtx, ourClient.search().forResource(theType).execute()) + .forEach(t -> { + ourClient.delete().resourceById(t.getIdElement()).execute(); + }); + } + @Test public void testTransactionDeleteWithDuplicateDeletes() throws Exception { myDaoConfig.setAllowInlineMatchUrlReferences(true); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderTransactionSearchR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderTransactionSearchR4Test.java index 7219a0025a4..2b77d67296d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderTransactionSearchR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderTransactionSearchR4Test.java @@ -2,12 +2,11 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.rp.r4.*; import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; import ca.uhn.fhir.rest.param.ReferenceParam; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java index fd793f2dcf7..96667b524f7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.entity.Search; 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.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.BaseIterator; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.rest.api.CacheControlDirective; @@ -30,6 +31,8 @@ import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -49,11 +52,12 @@ import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class SearchCoordinatorSvcImplTest { + private static final Logger ourLog = LoggerFactory.getLogger(SearchCoordinatorSvcImplTest.class); private static FhirContext ourCtx = FhirContext.forDstu3(); @Captor ArgumentCaptor> mySearchResultIterCaptor; @Mock - private IDao myCallingDao; + private IFhirResourceDao myCallingDao; @Mock private EntityManager myEntityManager; private int myExpectedNumberOfSearchBuildersCreated = 2; @@ -66,10 +70,12 @@ public class SearchCoordinatorSvcImplTest { @Mock private ISearchResultDao mySearchResultDao; private SearchCoordinatorSvcImpl mySvc; - @Mock private PlatformTransactionManager myTxManager; private DaoConfig myDaoConfig; + private Search myCurrentSearch; + @Mock + private DaoRegistry myDaoRegistry; @After public void after() { @@ -78,6 +84,7 @@ public class SearchCoordinatorSvcImplTest { @Before public void before() { + myCurrentSearch = null; mySvc = new SearchCoordinatorSvcImpl(); mySvc.setEntityManagerForUnitTest(myEntityManager); @@ -86,6 +93,7 @@ public class SearchCoordinatorSvcImplTest { mySvc.setSearchDaoForUnitTest(mySearchDao); mySvc.setSearchDaoIncludeForUnitTest(mySearchIncludeDao); mySvc.setSearchDaoResultForUnitTest(mySearchResultDao); + mySvc.setDaoRegistryForUnitTest(myDaoRegistry); myDaoConfig = new DaoConfig(); mySvc.setDaoConfigForUnitTest(myDaoConfig); @@ -158,17 +166,36 @@ public class SearchCoordinatorSvcImplTest { params.add("name", new StringParam("ANAME")); List pids = createPidSequence(10, 800); - IResultIterator iter = new SlowIterator(pids.iterator(), 1); - when(mySearchBuider.createQuery(Mockito.same(params), any(String.class))).thenReturn(iter); - + SlowIterator iter = new SlowIterator(pids.iterator(), 1); + when(mySearchBuider.createQuery(any(), any(String.class))).thenReturn(iter); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); + when(mySearchResultDao.findWithSearchUuid(any(), any())).thenAnswer(t -> { + List returnedValues = iter.getReturnedValues(); + Pageable page = (Pageable) t.getArguments()[1]; + int offset = (int) page.getOffset(); + int end = (int) (page.getOffset() + page.getPageSize()); + end = Math.min(end, returnedValues.size()); + offset = Math.min(offset, returnedValues.size()); + ourLog.info("findWithSearchUuid {} - {} out of {} values", offset, end, returnedValues.size()); + return new PageImpl<>(returnedValues.subList(offset, end)); + }); + IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective()); assertNotNull(result.getUuid()); assertEquals(null, result.size()); List resources; + when(mySearchDao.save(any())).thenAnswer(t -> { + Search search = (Search) t.getArguments()[0]; + myCurrentSearch = search; + return search; + }); + when(mySearchDao.findByUuid(any())).thenAnswer(t -> myCurrentSearch); + IFhirResourceDao dao = myCallingDao; + when(myDaoRegistry.getResourceDao(any(String.class))).thenReturn(dao); + resources = result.getResources(0, 100000); assertEquals(790, resources.size()); assertEquals("10", resources.get(0).getIdElement().getValueAsString()); @@ -178,7 +205,7 @@ public class SearchCoordinatorSvcImplTest { verify(mySearchDao, atLeastOnce()).save(searchCaptor.capture()); verify(mySearchResultDao, atLeastOnce()).saveAll(mySearchResultIterCaptor.capture()); - List allResults = new ArrayList(); + List allResults = new ArrayList<>(); for (Iterable next : mySearchResultIterCaptor.getAllValues()) { allResults.addAll(Lists.newArrayList(next)); } @@ -186,6 +213,8 @@ public class SearchCoordinatorSvcImplTest { assertEquals(790, allResults.size()); assertEquals(10, allResults.get(0).getResourcePid().longValue()); assertEquals(799, allResults.get(789).getResourcePid().longValue()); + + myExpectedNumberOfSearchBuildersCreated = 4; } @Test @@ -224,7 +253,7 @@ public class SearchCoordinatorSvcImplTest { List pids = createPidSequence(10, 800); IResultIterator iter = new SlowIterator(pids.iterator(), 2); when(mySearchBuider.createQuery(Mockito.same(params), any(String.class))).thenReturn(iter); - + when(mySearchDao.save(any())).thenAnswer(t -> t.getArguments()[0]); doAnswer(loadPids()).when(mySearchBuider).loadResourcesByPid(any(List.class), any(List.class), any(Set.class), anyBoolean(), any(EntityManager.class), any(FhirContext.class), same(myCallingDao)); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective()); @@ -257,12 +286,6 @@ public class SearchCoordinatorSvcImplTest { assertEquals("20", resources.get(0).getIdElement().getValueAsString()); assertEquals("29", resources.get(9).getIdElement().getValueAsString()); - provider = new PersistedJpaBundleProvider(result.getUuid(), myCallingDao); - resources = provider.getResources(20, 99999); - assertEquals(770, resources.size()); - assertEquals("30", resources.get(0).getIdElement().getValueAsString()); - assertEquals("799", resources.get(769).getIdElement().getValueAsString()); - myExpectedNumberOfSearchBuildersCreated = 4; } @@ -452,11 +475,19 @@ public class SearchCoordinatorSvcImplTest { } } + /** + * THIS CLASS IS FOR UNIT TESTS ONLY - It is delioberately inefficient + * and keeps things in memory. + *

    + * Don't use it in real code! + */ public static class SlowIterator extends BaseIterator implements IResultIterator { + private static final Logger ourLog = LoggerFactory.getLogger(SlowIterator.class); private final IResultIterator myResultIteratorWrap; private int myDelay; private Iterator myWrap; + private List myReturnedValues = new ArrayList<>(); public SlowIterator(Iterator theWrap, int theDelay) { myWrap = theWrap; @@ -470,9 +501,17 @@ public class SearchCoordinatorSvcImplTest { myDelay = theDelay; } + public List getReturnedValues() { + return myReturnedValues; + } + @Override public boolean hasNext() { - return myWrap.hasNext(); + boolean retVal = myWrap.hasNext(); + if (!retVal) { + ourLog.info("No more results remaining"); + } + return retVal; } @Override @@ -482,7 +521,9 @@ public class SearchCoordinatorSvcImplTest { } catch (InterruptedException e) { // ignore } - return myWrap.next(); + Long retVal = myWrap.next(); + myReturnedValues.add(retVal); + return retVal; } @Override 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 new file mode 100644 index 00000000000..1e7152417b6 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImplTest.java @@ -0,0 +1,303 @@ +package ca.uhn.fhir.jpa.search.reindex; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.BaseJpaTest; +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.data.IForcedIdDao; +import ca.uhn.fhir.jpa.dao.data.IResourceReindexJobDao; +import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; +import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.SliceImpl; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.*; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + + +public class ResourceReindexingSvcImplTest extends BaseJpaTest { + + private static FhirContext ourCtx = FhirContext.forR4(); + + @Mock + private PlatformTransactionManager myTxManager; + + private ResourceReindexingSvcImpl mySvc; + private DaoConfig myDaoConfig; + + @Mock + private DaoRegistry myDaoRegistry; + @Mock + private IForcedIdDao myForcedIdDao; + @Mock + private IResourceReindexJobDao myReindexJobDao; + @Mock + private IResourceTableDao myResourceTableDao; + @Mock + private IFhirResourceDao myResourceDao; + @Captor + private ArgumentCaptor myIdCaptor; + @Captor + private ArgumentCaptor myPageRequestCaptor; + @Captor + private ArgumentCaptor myTypeCaptor; + @Captor + private ArgumentCaptor myLowCaptor; + @Captor + private ArgumentCaptor myHighCaptor; + private ResourceReindexJobEntity mySingleJob; + + @Override + protected FhirContext getContext() { + return ourCtx; + } + + @Override + protected PlatformTransactionManager getTxManager() { + return myTxManager; + } + + @Before + public void before() { + myDaoConfig = new DaoConfig(); + myDaoConfig.setReindexThreadCount(2); + + mySvc = new ResourceReindexingSvcImpl(); + mySvc.setContextForUnitTest(ourCtx); + mySvc.setDaoConfigForUnitTest(myDaoConfig); + mySvc.setDaoRegistryForUnitTest(myDaoRegistry); + mySvc.setForcedIdDaoForUnitTest(myForcedIdDao); + mySvc.setReindexJobDaoForUnitTest(myReindexJobDao); + mySvc.setResourceTableDaoForUnitTest(myResourceTableDao); + mySvc.setTxManagerForUnitTest(myTxManager); + mySvc.start(); + } + + @Test + public void testReindexPassOnlyReturnsValuesAtLowThreshold() { + mockNothingToExpunge(); + mockSingleReindexingJob(null); + mockFetchFourResources(); + mockFinalResourceNeedsReindexing(); + + 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(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)); + + // 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 + public void testExpungeDeletedJobs() { + ResourceReindexJobEntity job = new ResourceReindexJobEntity(); + job.setIdForUnitTest(123L); + job.setDeleted(true); + when(myReindexJobDao.findAll(any(), eq(true))).thenReturn(Arrays.asList(job)); + + mySvc.forceReindexingPass(); + + verify(myReindexJobDao, times(1)).deleteById(eq(123L)); + } + + @Test + public void testReindexPassAllResources() { + mockNothingToExpunge(); + mockSingleReindexingJob(null); + mockFourResourcesNeedReindexing(); + mockFetchFourResources(); + + int count = mySvc.forceReindexingPass(); + assertEquals(4, count); + + // Make sure we reindexed all 4 resources + verify(myResourceDao, times(4)).reindex(any(), any()); + + // Make sure we updated the low threshold + verify(myReindexJobDao, times(1)).setThresholdLow(myIdCaptor.capture(), myLowCaptor.capture()); + assertEquals(123L, myIdCaptor.getValue().longValue()); + assertEquals(40 * DateUtils.MILLIS_PER_DAY, myLowCaptor.getValue().getTime()); + + // 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); + } + + @Test + public void testReindexPassPatients() { + mockNothingToExpunge(); + mockSingleReindexingJob("Patient"); + // Mock resource fetch + List values = Arrays.asList(0L, 1L, 2L, 3L); + when(myResourceTableDao.findIdsOfResourcesWithinUpdatedRangeOrderedFromOldest(myPageRequestCaptor.capture(), myTypeCaptor.capture(), myLowCaptor.capture(), myHighCaptor.capture())).thenReturn(new SliceImpl<>(values)); + // Mock fetching resources + long[] updatedTimes = new long[]{ + 10 * DateUtils.MILLIS_PER_DAY, + 20 * DateUtils.MILLIS_PER_DAY, + 40 * DateUtils.MILLIS_PER_DAY, + 30 * DateUtils.MILLIS_PER_DAY, + }; + String[] resourceTypes = new String[]{ + "Patient", + "Patient", + "Patient", + "Patient" + }; + List resources = Arrays.asList( + new Patient().setId("Patient/0/_history/1"), + new Patient().setId("Patient/1/_history/1"), + new Patient().setId("Patient/2/_history/1"), + new Patient().setId("Patient/3/_history/1") + ); + mockWhenResourceTableFindById(updatedTimes, resourceTypes); + when(myDaoRegistry.getResourceDao(eq("Patient"))).thenReturn(myResourceDao); + when(myDaoRegistry.getResourceDao(eq(Patient.class))).thenReturn(myResourceDao); + when(myDaoRegistry.getResourceDao(eq("Observation"))).thenReturn(myResourceDao); + when(myDaoRegistry.getResourceDao(eq(Observation.class))).thenReturn(myResourceDao); + when(myResourceDao.read(any(), any(), anyBoolean())).thenAnswer(t->{ + IIdType id = (IIdType) t.getArguments()[0]; + return resources.get(id.getIdPartAsLong().intValue()); + }); + + + int count = mySvc.forceReindexingPass(); + assertEquals(4, count); + + // Make sure we reindexed all 4 resources + verify(myResourceDao, times(4)).reindex(any(), any()); + + // Make sure we updated the low threshold + verify(myReindexJobDao, times(1)).setThresholdLow(myIdCaptor.capture(), myLowCaptor.capture()); + assertEquals(123L, myIdCaptor.getValue().longValue()); + assertEquals(40 * DateUtils.MILLIS_PER_DAY, myLowCaptor.getValue().getTime()); + + // 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); + } + + private void mockWhenResourceTableFindById(long[] theUpdatedTimes, String[] theResourceTypes) { + when(myResourceTableDao.findById(any())).thenAnswer(t -> { + ResourceTable retVal = new ResourceTable(); + Long id = (Long) t.getArguments()[0]; + retVal.setId(id); + retVal.setResourceType(theResourceTypes[id.intValue()]); + retVal.setUpdated(new Date(theUpdatedTimes[id.intValue()])); + return Optional.of(retVal); + }); + } + + private void mockFetchFourResources() { + // Mock fetching resources + long[] updatedTimes = new long[]{ + 10 * DateUtils.MILLIS_PER_DAY, + 20 * DateUtils.MILLIS_PER_DAY, + 40 * DateUtils.MILLIS_PER_DAY, + 30 * DateUtils.MILLIS_PER_DAY, + }; + String[] resourceTypes = new String[]{ + "Patient", + "Patient", + "Observation", + "Observation" + }; + List resources = Arrays.asList( + new Patient().setId("Patient/0/_history/1"), + new Patient().setId("Patient/1/_history/1"), + new Observation().setId("Observation/2/_history/1"), + new Observation().setId("Observation/3/_history/1") + ); + mockWhenResourceTableFindById(updatedTimes, resourceTypes); + when(myDaoRegistry.getResourceDao(eq("Patient"))).thenReturn(myResourceDao); + when(myDaoRegistry.getResourceDao(eq(Patient.class))).thenReturn(myResourceDao); + when(myDaoRegistry.getResourceDao(eq("Observation"))).thenReturn(myResourceDao); + when(myDaoRegistry.getResourceDao(eq(Observation.class))).thenReturn(myResourceDao); + when(myResourceDao.read(any(), any(), anyBoolean())).thenAnswer(t->{ + IIdType id = (IIdType) t.getArguments()[0]; + return resources.get(id.getIdPartAsLong().intValue()); + }); + } + + private void mockFourResourcesNeedReindexing() { + // Mock resource fetch + List values = Arrays.asList(0L, 1L, 2L, 3L); + 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) { + // Mock the reindexing job + mySingleJob = new ResourceReindexJobEntity(); + mySingleJob.setIdForUnitTest(123L); + mySingleJob.setThresholdHigh(DateUtils.addMinutes(new Date(), 1)); + mySingleJob.setResourceType(theResourceType); + when(myReindexJobDao.findAll(any(), eq(false))).thenReturn(Arrays.asList(mySingleJob)); + } + + private void mockNothingToExpunge() { + // Nothing to expunge + when(myReindexJobDao.findAll(any(), eq(true))).thenReturn(new ArrayList<>()); + } +} 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/RestHookTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java index ee96aefb4f4..fcaa6964165 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java @@ -11,7 +11,7 @@ import ca.uhn.fhir.rest.api.Constants; 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.PortUtil; import com.google.common.collect.Lists; import org.eclipse.jetty.server.Server; @@ -24,6 +24,7 @@ import org.junit.*; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static org.junit.Assert.assertEquals; @@ -35,14 +36,14 @@ import static org.junit.Assert.fail; public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestDstu3Test.class); - private static List ourCreatedObservations = Lists.newArrayList(); + private static List ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList()); private static int ourListenerPort; private static RestfulServer ourListenerRestServer; private static Server ourListenerServer; private static String ourListenerServerBase; - private static List ourUpdatedObservations = Lists.newArrayList(); - private static List ourContentTypes = new ArrayList<>(); - private List mySubscriptionIds = new ArrayList<>(); + private static List ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); + private static List ourContentTypes = Collections.synchronizedList(new ArrayList<>()); + private List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); @After public void afterUnregisterRestHookListener() { @@ -330,9 +331,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { waitForSize(0, ourUpdatedObservations); } - // TODO: Reenable this @Test - @Ignore public void testRestHookSubscriptionInvalidCriteria() throws Exception { String payload = "application/xml"; @@ -341,8 +340,8 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { try { createSubscription(criteria1, payload, ourListenerServerBase); fail(); - } catch (InvalidRequestException e) { - assertEquals("HTTP 400 Bad Request: Invalid criteria: Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage()); + } catch (UnprocessableEntityException e) { + assertEquals("HTTP 422 Unprocessable Entity: Invalid subscription criteria submitted: Observation?codeeeee=SNOMED-CT Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java index 2ea0529282a..c542bb0f9ab 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java @@ -1,4 +1,3 @@ - package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.context.FhirContext; @@ -27,6 +26,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.*; +import java.util.Collections; import java.util.List; /** @@ -34,13 +34,13 @@ import java.util.List; */ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends BaseResourceProviderDstu2Test { - private static List ourCreatedObservations = Lists.newArrayList(); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.class); + private static List ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList()); private static int ourListenerPort; private static RestfulServer ourListenerRestServer; private static Server ourListenerServer; private static String ourListenerServerBase; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.class); - private static List ourUpdatedObservations = Lists.newArrayList(); + private static List ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); @After public void afterUnregisterRestHookListener() { @@ -50,7 +50,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B ourClient.delete().resourceConditionalByUrl("Subscription?status=active").execute(); ourLog.info("Done deleting all subscriptions"); myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); - + myDaoConfig.getInterceptors().remove(ourRestHookSubscriptionInterceptor); } @@ -127,7 +127,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B waitForQueueToDrain(); waitForSize(0, ourCreatedObservations); waitForSize(1, ourUpdatedObservations); - + Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId()); Assert.assertNotNull(subscriptionTemp); @@ -141,8 +141,8 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B waitForQueueToDrain(); waitForSize(0, ourCreatedObservations); waitForSize(3, ourUpdatedObservations); - - ourClient.delete().resourceById(new IdDt("Subscription/"+ subscription2.getId())).execute(); + + ourClient.delete().resourceById(new IdDt("Subscription/" + subscription2.getId())).execute(); Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); @@ -200,7 +200,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B waitForQueueToDrain(); waitForSize(0, ourCreatedObservations); waitForSize(1, ourUpdatedObservations); - + Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId()); Assert.assertNotNull(subscriptionTemp); @@ -214,8 +214,8 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B waitForQueueToDrain(); waitForSize(0, ourCreatedObservations); waitForSize(3, ourUpdatedObservations); - - ourClient.delete().resourceById(new IdDt("Subscription/"+ subscription2.getId())).execute(); + + ourClient.delete().resourceById(new IdDt("Subscription/" + subscription2.getId())).execute(); Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); @@ -256,7 +256,29 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B Assert.assertFalse(observation2.getId().isEmpty()); } - + public static class ObservationListener implements IResourceProvider { + + @Create + public MethodOutcome create(@ResourceParam Observation theObservation) { + ourLog.info("Received Listener Create"); + ourCreatedObservations.add(theObservation); + return new MethodOutcome(new IdDt("Observation/1"), true); + } + + @Override + public Class getResourceType() { + return Observation.class; + } + + @Update + public MethodOutcome update(@ResourceParam Observation theObservation) { + ourLog.info("Received Listener Update"); + ourUpdatedObservations.add(theObservation); + return new MethodOutcome(new IdDt("Observation/1"), false); + } + + } + @BeforeClass public static void startListenerServer() throws Exception { ourListenerPort = PortUtil.findFreePort(); @@ -284,27 +306,4 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B ourListenerServer.stop(); } - public static class ObservationListener implements IResourceProvider { - - @Create - public MethodOutcome create(@ResourceParam Observation theObservation) { - ourLog.info("Received Listener Create"); - ourCreatedObservations.add(theObservation); - return new MethodOutcome(new IdDt("Observation/1"), true); - } - - @Override - public Class getResourceType() { - return Observation.class; - } - - @Update - public MethodOutcome update(@ResourceParam Observation theObservation) { - ourLog.info("Received Listener Update"); - ourUpdatedObservations.add(theObservation); - return new MethodOutcome(new IdDt("Observation/1"), false); - } - - } - } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java index fdab2166597..5064960ee91 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.subscription; import static org.junit.Assert.*; +import java.util.Collections; import java.util.List; import org.eclipse.jetty.server.Server; @@ -29,13 +30,13 @@ import ca.uhn.fhir.rest.server.RestfulServer; */ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test extends BaseResourceProviderDstu3Test { - private static List ourCreatedObservations = Lists.newArrayList(); + private static List ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList()); private static int ourListenerPort; private static RestfulServer ourListenerRestServer; private static Server ourListenerServer; private static String ourListenerServerBase; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.class); - private static List ourUpdatedObservations = Lists.newArrayList(); + private static List ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); @Override protected boolean shouldLogClient() { 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/SubscriptionMatcherInMemoryTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR4.java new file mode 100644 index 00000000000..943987dfc5b --- /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.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +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/BaseSubscriptionsR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/BaseSubscriptionsR4Test.java new file mode 100644 index 00000000000..04f6a800449 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/BaseSubscriptionsR4Test.java @@ -0,0 +1,242 @@ +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.dao.DaoRegistry; +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.rest.annotation.Create; +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.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +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; +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; +import java.util.Enumeration; +import java.util.List; + +@Ignore +public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSubscriptionsR4Test.class); + + + private static int ourListenerPort; + private static RestfulServer ourListenerRestServer; + private static Server ourListenerServer; + protected static List ourContentTypes = Collections.synchronizedList(new ArrayList<>()); + protected static List ourHeaders = Collections.synchronizedList(new ArrayList<>()); + private static SingleQueryCountHolder ourCountHolder; + + @Autowired + private SingleQueryCountHolder myCountHolder; + @Autowired + protected DaoConfig myDaoConfig; + @Autowired + private DaoRegistry myDaoRegistry; + + protected CountingInterceptor myCountingInterceptor; + + protected static List ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList()); + protected static List ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); + protected static String ourListenerServerBase; + + protected List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); + + + @After + public void afterUnregisterRestHookListener() { + BaseSubscriptionInterceptor.setForcePayloadEncodeAndDecodeForUnitTests(false); + + for (IIdType next : mySubscriptionIds) { + IIdType nextId = next.toUnqualifiedVersionless(); + ourLog.info("Deleting: {}", nextId); + ourClient.delete().resourceById(nextId).execute(); + } + mySubscriptionIds.clear(); + + myDaoConfig.setAllowMultipleDelete(true); + ourLog.info("Deleting all subscriptions"); + ourClient.delete().resourceConditionalByUrl("Subscription?status=active").execute(); + ourClient.delete().resourceConditionalByUrl("Observation?code:missing=false").execute(); + ourLog.info("Done deleting all subscriptions"); + myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); + + ourRestServer.unregisterInterceptor(getRestHookSubscriptionInterceptor()); + } + + @Before + public void beforeRegisterRestHookListener() { + ourRestServer.registerInterceptor(getRestHookSubscriptionInterceptor()); + } + + @Before + public void beforeReset() throws Exception { + ourCreatedObservations.clear(); + ourUpdatedObservations.clear(); + ourContentTypes.clear(); + ourHeaders.clear(); + + // Delete all Subscriptions + Bundle allSubscriptions = ourClient.search().forResource(Subscription.class).returnBundle(Bundle.class).execute(); + for (IBaseResource next : BundleUtil.toListOfResources(myFhirCtx, allSubscriptions)) { + ourClient.delete().resource(next).execute(); + } + waitForRegisteredSubscriptionCount(0); + + ExecutorSubscribableChannel processingChannel = (ExecutorSubscribableChannel) getRestHookSubscriptionInterceptor().getProcessingChannel(); + processingChannel.setInterceptors(new ArrayList<>()); + myCountingInterceptor = new CountingInterceptor(); + processingChannel.addInterceptor(myCountingInterceptor); + } + + + protected Subscription createSubscription(String theCriteria, String thePayload) throws InterruptedException { + Subscription subscription = newSubscription(theCriteria, thePayload); + + MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute(); + subscription.setId(methodOutcome.getId().getIdPart()); + mySubscriptionIds.add(methodOutcome.getId()); + + return subscription; + } + + protected Subscription newSubscription(String theCriteria, String thePayload) { + Subscription subscription = new Subscription(); + subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); + subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED); + subscription.setCriteria(theCriteria); + + Subscription.SubscriptionChannelComponent channel = subscription.getChannel(); + channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); + channel.setPayload(thePayload); + channel.setEndpoint(ourListenerServerBase); + return subscription; + } + + + protected void waitForQueueToDrain() throws InterruptedException { + RestHookTestDstu2Test.waitForQueueToDrain(getRestHookSubscriptionInterceptor()); + } + + @PostConstruct + public void initializeOurCountHolder() { + ourCountHolder = myCountHolder; + } + + + protected Observation sendObservation(String code, String system) { + Observation observation = new Observation(); + CodeableConcept codeableConcept = new CodeableConcept(); + observation.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode(code); + coding.setSystem(system); + + observation.setStatus(Observation.ObservationStatus.FINAL); + + MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); + + String observationId = methodOutcome.getId().getIdPart(); + observation.setId(observationId); + + return observation; + } + + + + public static class ObservationListener implements IResourceProvider { + + @Create + public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { + ourLog.info("Received Listener Create"); + ourContentTypes.add(theRequest.getHeader(ca.uhn.fhir.rest.api.Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); + ourCreatedObservations.add(theObservation); + extractHeaders(theRequest); + return new MethodOutcome(new IdType("Observation/1"), true); + } + + private void extractHeaders(HttpServletRequest theRequest) { + java.util.Enumeration headerNamesEnum = theRequest.getHeaderNames(); + while (headerNamesEnum.hasMoreElements()) { + String nextName = headerNamesEnum.nextElement(); + Enumeration valueEnum = theRequest.getHeaders(nextName); + while (valueEnum.hasMoreElements()) { + String nextValue = valueEnum.nextElement(); + ourHeaders.add(nextName + ": " + nextValue); + } + } + } + + @Override + public Class getResourceType() { + return Observation.class; + } + + @Update + public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { + ourLog.info("Received Listener Update"); + ourUpdatedObservations.add(theObservation); + ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); + extractHeaders(theRequest); + return new MethodOutcome(new IdType("Observation/1"), false); + } + + } + + @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(); + ourListenerRestServer = new RestfulServer(FhirContext.forR4()); + ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context"; + + ObservationListener obsListener = new ObservationListener(); + ourListenerRestServer.setResourceProviders(obsListener); + + ourListenerServer = new Server(ourListenerPort); + + ServletContextHandler proxyHandler = new ServletContextHandler(); + proxyHandler.setContextPath("/"); + + ServletHolder servletHolder = new ServletHolder(); + servletHolder.setServlet(ourListenerRestServer); + proxyHandler.addServlet(servletHolder, "/fhir/context/*"); + + ourListenerServer.setHandler(proxyHandler); + ourListenerServer.start(); + } + + @AfterClass + public static void stopListenerServer() throws Exception { + ourListenerServer.stop(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/FhirClientSearchParamProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/FhirClientSearchParamProviderTest.java new file mode 100644 index 00000000000..f75f67152f5 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/FhirClientSearchParamProviderTest.java @@ -0,0 +1,88 @@ +package ca.uhn.fhir.jpa.subscription.r4; + +import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test; +import ca.uhn.fhir.jpa.searchparam.registry.BaseSearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.subscription.FhirClientSearchParamProvider; +import ca.uhn.fhir.rest.api.MethodOutcome; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.SearchParameter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.junit.Assert.assertEquals; + + +public class FhirClientSearchParamProviderTest extends BaseSubscriptionsR4Test { + @Autowired + BaseSearchParamRegistry mySearchParamRegistry; + @Autowired + ISearchParamProvider origSearchParamProvider; + + @Before + public void useFhirClientSearchParamProvider() { + mySearchParamRegistry.setSearchParamProvider(new FhirClientSearchParamProvider(ourClient)); + } + + @After + public void revert() { + mySearchParamRegistry.setSearchParamProvider(origSearchParamProvider); + } + + @Test + public void testCustomSearchParam() throws Exception { + String criteria = "Observation?accessType=Catheter,PD%20Catheter"; + + SearchParameter sp = new SearchParameter(); + sp.addBase("Observation"); + sp.setCode("accessType"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("Observation.extension('Observation#accessType')"); + sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(sp); + mySearchParamRegsitry.forceRefresh(); + createSubscription(criteria, "application/json"); + waitForRegisteredSubscriptionCount(1); + + { + Observation observation = new Observation(); + observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("Catheter")); + MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); + assertEquals(true, methodOutcome.getCreated()); + waitForQueueToDrain(); + waitForSize(1, ourUpdatedObservations); + } + { + Observation observation = new Observation(); + observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("PD Catheter")); + MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); + assertEquals(true, methodOutcome.getCreated()); + waitForQueueToDrain(); + waitForSize(2, ourUpdatedObservations); + } + { + Observation observation = new Observation(); + MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); + assertEquals(true, methodOutcome.getCreated()); + waitForQueueToDrain(); + waitForSize(2, ourUpdatedObservations); + } + { + Observation observation = new Observation(); + observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("XXX")); + MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); + assertEquals(true, methodOutcome.getCreated()); + waitForQueueToDrain(); + waitForSize(2, ourUpdatedObservations); + } + + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookActivatesPreExistingSubscriptionsR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookActivatesPreExistingSubscriptionsR4Test.java index 031140227c4..8d0c3a44baa 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookActivatesPreExistingSubscriptionsR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookActivatesPreExistingSubscriptionsR4Test.java @@ -23,10 +23,12 @@ import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourceProviderR4Test { @@ -35,9 +37,9 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc private static RestfulServer ourListenerRestServer; private static String ourListenerServerBase; private static Server ourListenerServer; - private static List ourUpdatedObservations = Lists.newArrayList(); - private static List ourContentTypes = new ArrayList<>(); - private static List ourHeaders = new ArrayList<>(); + private static List ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); + private static List ourContentTypes = Collections.synchronizedList(new ArrayList<>()); + private static List ourHeaders = Collections.synchronizedList(new ArrayList<>()); @After public void afterResetSubscriptionActivatingInterceptor() { @@ -123,33 +125,6 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc } } - @BeforeClass - public static void startListenerServer() throws Exception { - ourListenerPort = PortUtil.findFreePort(); - ourListenerRestServer = new RestfulServer(FhirContext.forR4()); - ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context"; - - ObservationListener obsListener = new ObservationListener(); - ourListenerRestServer.setResourceProviders(obsListener); - - ourListenerServer = new Server(ourListenerPort); - - ServletContextHandler proxyHandler = new ServletContextHandler(); - proxyHandler.setContextPath("/"); - - ServletHolder servletHolder = new ServletHolder(); - servletHolder.setServlet(ourListenerRestServer); - proxyHandler.addServlet(servletHolder, "/fhir/context/*"); - - ourListenerServer.setHandler(proxyHandler); - ourListenerServer.start(); - } - - @AfterClass - public static void stopListenerServer() throws Exception { - ourListenerServer.stop(); - } - public static class ObservationListener implements IResourceProvider { @@ -181,4 +156,31 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc } + @BeforeClass + public static void startListenerServer() throws Exception { + ourListenerPort = PortUtil.findFreePort(); + ourListenerRestServer = new RestfulServer(FhirContext.forR4()); + ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context"; + + ObservationListener obsListener = new ObservationListener(); + ourListenerRestServer.setResourceProviders(obsListener); + + ourListenerServer = new Server(ourListenerPort); + + ServletContextHandler proxyHandler = new ServletContextHandler(); + proxyHandler.setContextPath("/"); + + ServletHolder servletHolder = new ServletHolder(); + servletHolder.setServlet(ourListenerRestServer); + proxyHandler.addServlet(servletHolder, "/fhir/context/*"); + + ourListenerServer.setHandler(proxyHandler); + ourListenerServer.start(); + } + + @AfterClass + public static void stopListenerServer() throws Exception { + ourListenerServer.stop(); + } + } 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 00b0f31eda1..81ec08ec3c7 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 @@ -2,135 +2,51 @@ 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.dao.DaoRegistry; +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; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.Constants; 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; +import org.hl7.fhir.instance.model.api.IBaseBundle; 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; import java.util.Enumeration; 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 */ -public class RestHookTestR4Test extends BaseResourceProviderR4Test { - - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestDstu2Test.class); - private static List ourCreatedObservations = Lists.newArrayList(); - private static int ourListenerPort; - private static RestfulServer ourListenerRestServer; - private static Server ourListenerServer; - private static String ourListenerServerBase; - private static List ourUpdatedObservations = Lists.newArrayList(); - private static List ourContentTypes = new ArrayList<>(); - private static List ourHeaders = new ArrayList<>(); - private List mySubscriptionIds = new ArrayList<>(); - private CountingInterceptor myCountingInterceptor; - - @After - public void afterUnregisterRestHookListener() { - for (IIdType next : mySubscriptionIds) { - IIdType nextId = next.toUnqualifiedVersionless(); - ourLog.info("Deleting: {}", nextId); - ourClient.delete().resourceById(nextId).execute(); - } - mySubscriptionIds.clear(); - - myDaoConfig.setAllowMultipleDelete(true); - ourLog.info("Deleting all subscriptions"); - ourClient.delete().resourceConditionalByUrl("Subscription?status=active").execute(); - ourClient.delete().resourceConditionalByUrl("Observation?code:missing=false").execute(); - ourLog.info("Done deleting all subscriptions"); - myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); - - ourRestServer.unregisterInterceptor(getRestHookSubscriptionInterceptor()); - } - - @Before - public void beforeRegisterRestHookListener() { - ourRestServer.registerInterceptor(getRestHookSubscriptionInterceptor()); - } - - @Before - public void beforeReset() throws Exception { - ourCreatedObservations.clear(); - ourUpdatedObservations.clear(); - ourContentTypes.clear(); - ourHeaders.clear(); - - // Delete all Subscriptions - Bundle allSubscriptions = ourClient.search().forResource(Subscription.class).returnBundle(Bundle.class).execute(); - for (IBaseResource next : BundleUtil.toListOfResources(myFhirCtx, allSubscriptions)) { - ourClient.delete().resource(next).execute(); - } - waitForRegisteredSubscriptionCount(0); - - ExecutorSubscribableChannel processingChannel = (ExecutorSubscribableChannel) getRestHookSubscriptionInterceptor().getProcessingChannel(); - processingChannel.setInterceptors(new ArrayList<>()); - myCountingInterceptor = new CountingInterceptor(); - processingChannel.addInterceptor(myCountingInterceptor); - } - - private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException { - Subscription subscription = new Subscription(); - subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); - subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED); - subscription.setCriteria(theCriteria); - - Subscription.SubscriptionChannelComponent channel = subscription.getChannel(); - 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; - } - - private Observation sendObservation(String code, String system) { - Observation observation = new Observation(); - CodeableConcept codeableConcept = new CodeableConcept(); - observation.setCode(codeableConcept); - Coding coding = codeableConcept.addCoding(); - coding.setCode(code); - coding.setSystem(system); - - observation.setStatus(Observation.ObservationStatus.FINAL); - - MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); - - String observationId = methodOutcome.getId().getIdPart(); - observation.setId(observationId); - - return observation; - } +public class RestHookTestR4Test extends BaseSubscriptionsR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestR4Test.class); @Test public void testRestHookSubscriptionApplicationFhirJson() throws Exception { @@ -140,8 +56,8 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - createSubscription(criteria1, payload, ourListenerServerBase); - createSubscription(criteria2, payload, ourListenerServerBase); + createSubscription(criteria1, payload); + createSubscription(criteria2, payload); waitForRegisteredSubscriptionCount(2); sendObservation(code, "SNOMED-CT"); @@ -157,7 +73,7 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { public void testActiveSubscriptionShouldntReActivate() throws Exception { String criteria = "Observation?code=111111111&_format=xml"; String payload = "application/fhir+json"; - createSubscription(criteria, payload, ourListenerServerBase); + createSubscription(criteria, payload); waitForRegisteredSubscriptionCount(1); for (int i = 0; i < 5; i++) { @@ -174,8 +90,8 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - createSubscription(criteria1, payload, ourListenerServerBase); - createSubscription(criteria2, payload, ourListenerServerBase); + createSubscription(criteria1, payload); + createSubscription(criteria2, payload); waitForRegisteredSubscriptionCount(2); Observation obs = sendObservation(code, "SNOMED-CT"); @@ -207,7 +123,7 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; waitForRegisteredSubscriptionCount(0); - Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase); + Subscription subscription1 = createSubscription(criteria1, payload); waitForRegisteredSubscriptionCount(1); int modCount = myCountingInterceptor.getSentCount(); @@ -242,8 +158,8 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase); - Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase); + Subscription subscription1 = createSubscription(criteria1, payload); + Subscription subscription2 = createSubscription(criteria2, payload); waitForRegisteredSubscriptionCount(2); Observation observation1 = sendObservation(code, "SNOMED-CT"); @@ -307,9 +223,89 @@ 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 testRestHookSubscriptionApplicationJsonDatabase() throws Exception { + // Same test as above, but now run it using database matching + myDaoConfig.setEnableInMemorySubscriptionMatching(false); + String payload = "application/json"; + + String code = "1000000050"; + String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; + String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; + + Subscription subscription1 = createSubscription(criteria1, payload); + Subscription subscription2 = createSubscription(criteria2, payload); + waitForRegisteredSubscriptionCount(2); + + Observation observation1 = sendObservation(code, "SNOMED-CT"); + + // Should see 1 subscription notification + waitForQueueToDrain(); + waitForSize(0, ourCreatedObservations); + waitForSize(1, ourUpdatedObservations); + assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); + + assertEquals("1", ourUpdatedObservations.get(0).getIdElement().getVersionIdPart()); + + Subscription subscriptionTemp = ourClient.read(Subscription.class, subscription2.getId()); + Assert.assertNotNull(subscriptionTemp); + + subscriptionTemp.setCriteria(criteria1); + ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute(); + waitForQueueToDrain(); + + Observation observation2 = sendObservation(code, "SNOMED-CT"); + waitForQueueToDrain(); + + // Should see two subscription notifications + waitForSize(0, ourCreatedObservations); + waitForSize(3, ourUpdatedObservations); + + ourClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute(); + waitForQueueToDrain(); + + Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); + waitForQueueToDrain(); + + // Should see only one subscription notification + waitForSize(0, ourCreatedObservations); + waitForSize(4, ourUpdatedObservations); + + Observation observation3 = ourClient.read(Observation.class, observationTemp3.getId()); + CodeableConcept codeableConcept = new CodeableConcept(); + observation3.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode(code + "111"); + coding.setSystem("SNOMED-CT"); + ourClient.update().resource(observation3).withId(observation3.getIdElement()).execute(); + + // Should see no subscription notification + waitForQueueToDrain(); + waitForSize(0, ourCreatedObservations); + waitForSize(4, ourUpdatedObservations); + + Observation observation3a = ourClient.read(Observation.class, observationTemp3.getId()); + + CodeableConcept codeableConcept1 = new CodeableConcept(); + observation3a.setCode(codeableConcept1); + Coding coding1 = codeableConcept1.addCoding(); + coding1.setCode(code); + coding1.setSystem("SNOMED-CT"); + ourClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute(); + + // Should see only one subscription notification + waitForQueueToDrain(); + waitForSize(0, ourCreatedObservations); + waitForSize(5, ourUpdatedObservations); + + assertFalse(subscription1.getId().equals(subscription2.getId())); + assertFalse(observation1.getId().isEmpty()); + assertFalse(observation2.getId().isEmpty()); } @Test @@ -320,8 +316,8 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase); - Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase); + Subscription subscription1 = createSubscription(criteria1, payload); + Subscription subscription2 = createSubscription(criteria2, payload); waitForRegisteredSubscriptionCount(2); ourLog.info("** About to send obervation"); @@ -381,9 +377,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); + 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 @@ -395,7 +444,7 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { ourLog.info("** About to create non-matching subscription"); - Subscription subscription2 = createSubscription(criteriaBad, payload, ourListenerServerBase); + Subscription subscription2 = createSubscription(criteriaBad, payload); ourLog.info("** About to send observation that wont match"); @@ -439,8 +488,8 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase); - Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase); + Subscription subscription1 = createSubscription(criteria1, payload); + Subscription subscription2 = createSubscription(criteria2, payload); waitForRegisteredSubscriptionCount(2); Observation observation1 = sendObservation(code, "SNOMED-CT"); @@ -461,7 +510,7 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { String criteria1 = "Observation?codeeeee=SNOMED-CT"; try { - createSubscription(criteria1, payload, ourListenerServerBase); + createSubscription(criteria1, payload); fail(); } catch (InvalidRequestException e) { assertEquals("HTTP 400 Bad Request: Invalid criteria: Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage()); @@ -476,7 +525,7 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; // Add some headers, and we'll also turn back to requested status for fun - Subscription subscription = createSubscription(criteria1, payload, ourListenerServerBase); + Subscription subscription = createSubscription(criteria1, payload); waitForRegisteredSubscriptionCount(1); subscription.getChannel().addHeader("X-Foo: FOO"); @@ -503,7 +552,7 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - Subscription subscription = createSubscription(criteria1, payload, ourListenerServerBase); + Subscription subscription = createSubscription(criteria1, payload); waitForRegisteredSubscriptionCount(1); sendObservation(code, "SNOMED-CT"); @@ -528,74 +577,109 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { } - private void waitForQueueToDrain() throws InterruptedException { - 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); + ourClient.create().resource(subscription).execute(); } - public static class ObservationListener implements IResourceProvider { + @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); + ourClient.create().resource(subscription).execute(); + } - @Create - public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { - ourLog.info("Received Listener Create"); - ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); - ourCreatedObservations.add(theObservation); - extractHeaders(theRequest); - return new MethodOutcome(new IdType("Observation/1"), true); + @Test(expected = UnprocessableEntityException.class) + public void testInvalidBodySiteParam() { + String payload = "application/fhir+json"; + String criteriabad = "BodySite?accessType=Catheter"; + Subscription subscription = newSubscription(criteriabad, payload); + ourClient.create().resource(subscription).execute(); + } + + @Test + public void testGoodSubscriptionPersists() { + assertEquals(0, subsciptionCount()); + String payload = "application/fhir+json"; + String criteriaGood = "Patient?gender=male"; + Subscription subscription = newSubscription(criteriaGood, payload); + ourClient.create().resource(subscription).execute(); + assertEquals(1, subsciptionCount()); + } + + private int subsciptionCount() { + IBaseBundle found = ourClient.search().forResource(Subscription.class).cacheControl(new CacheControlDirective().setNoCache(true)).execute(); + return toUnqualifiedVersionlessIdValues(found).size(); + } + + @Test + public void testBadSubscriptionDoesntPersist() { + assertEquals(0, subsciptionCount()); + String payload = "application/fhir+json"; + String criteriaBad = "BodySite?accessType=Catheter"; + Subscription subscription = newSubscription(criteriaBad, payload); + try { + ourClient.create().resource(subscription).execute(); + } catch (UnprocessableEntityException e) { + ourLog.info("Expected exception", e); } + assertEquals(0, subsciptionCount()); + } - private void extractHeaders(HttpServletRequest theRequest) { - Enumeration headerNamesEnum = theRequest.getHeaderNames(); - while (headerNamesEnum.hasMoreElements()) { - String nextName = headerNamesEnum.nextElement(); - Enumeration valueEnum = theRequest.getHeaders(nextName); - while (valueEnum.hasMoreElements()) { - String nextValue = valueEnum.nextElement(); - ourHeaders.add(nextName + ": " + nextValue); - } - } + @Test + public void testCustomSearchParam() throws Exception { + String criteria = "Observation?accessType=Catheter,PD%20Catheter"; + + SearchParameter sp = new SearchParameter(); + sp.addBase("Observation"); + sp.setCode("accessType"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("Observation.extension('Observation#accessType')"); + sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(sp); + mySearchParamRegsitry.forceRefresh(); + createSubscription(criteria, "application/json"); + waitForRegisteredSubscriptionCount(1); + + { + Observation bodySite = new Observation(); + bodySite.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("Catheter")); + MethodOutcome methodOutcome = ourClient.create().resource(bodySite).execute(); + assertEquals(true, methodOutcome.getCreated()); + waitForQueueToDrain(); + waitForSize(1, ourUpdatedObservations); } - - @Override - public Class getResourceType() { - return Observation.class; + { + Observation observation = new Observation(); + observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("PD Catheter")); + MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); + assertEquals(true, methodOutcome.getCreated()); + waitForQueueToDrain(); + waitForSize(2, ourUpdatedObservations); } - - @Update - public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { - ourLog.info("Received Listener Update"); - ourUpdatedObservations.add(theObservation); - ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); - extractHeaders(theRequest); - return new MethodOutcome(new IdType("Observation/1"), false); + { + Observation observation = new Observation(); + MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); + assertEquals(true, methodOutcome.getCreated()); + waitForQueueToDrain(); + waitForSize(2, ourUpdatedObservations); + } + { + Observation observation = new Observation(); + observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("XXX")); + MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); + assertEquals(true, methodOutcome.getCreated()); + waitForQueueToDrain(); + waitForSize(2, ourUpdatedObservations); } } - @BeforeClass - public static void startListenerServer() throws Exception { - ourListenerPort = PortUtil.findFreePort(); - ourListenerRestServer = new RestfulServer(FhirContext.forR4()); - ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context"; - ObservationListener obsListener = new ObservationListener(); - ourListenerRestServer.setResourceProviders(obsListener); - - ourListenerServer = new Server(ourListenerPort); - - ServletContextHandler proxyHandler = new ServletContextHandler(); - proxyHandler.setContextPath("/"); - - ServletHolder servletHolder = new ServletHolder(); - servletHolder.setServlet(ourListenerRestServer); - proxyHandler.addServlet(servletHolder, "/fhir/context/*"); - - ourListenerServer.setHandler(proxyHandler); - ourListenerServer.start(); - } - - @AfterClass - public static void stopListenerServer() throws Exception { - ourListenerServer.stop(); - } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java index 9017744687c..d1f95a93a7d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java @@ -1,8 +1,10 @@ package ca.uhn.fhir.jpa.subscription.r4; +import java.util.Collections; import java.util.List; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; @@ -27,13 +29,13 @@ import ca.uhn.fhir.rest.server.RestfulServer; */ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends BaseResourceProviderR4Test { - private static List ourCreatedObservations = Lists.newArrayList(); + private static List ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList()); private static int ourListenerPort; private static RestfulServer ourListenerRestServer; private static Server ourListenerServer; private static String ourListenerServerBase; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.class); - private static List ourUpdatedObservations = Lists.newArrayList(); + private static List ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); @Override protected boolean shouldLogClient() { @@ -141,7 +143,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base waitForSize(0, ourCreatedObservations); waitForSize(3, ourUpdatedObservations); - ourClient.delete().resourceById(new IdDt("Subscription", subscription2.getId())).execute(); + ourClient.delete().resourceById(new IdDt(ResourceTypeEnum.SUBSCRIPTION.getCode(), subscription2.getId())).execute(); Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookWithEventDefinitionR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookWithEventDefinitionR4Test.java index ace5d640492..8e95fa6506f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookWithEventDefinitionR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookWithEventDefinitionR4Test.java @@ -13,6 +13,7 @@ import org.junit.Test; import org.slf4j.Logger; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -35,13 +36,13 @@ import java.util.List; public class RestHookWithEventDefinitionR4Test extends BaseResourceProviderR4Test { private static final Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookWithEventDefinitionR4Test.class); - private static List ourUpdatedObservations = Lists.newArrayList(); - private static List ourContentTypes = new ArrayList<>(); - private static List ourHeaders = new ArrayList<>(); - private static List ourCreatedObservations = Lists.newArrayList(); + 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 List ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList()); private String myPatientId; private String mySubscriptionId; - private List mySubscriptionIds = new ArrayList<>(); + private List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); @Override @After diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu3Test.java index 86908943ce1..cbaa7c6fbd2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu3Test.java @@ -3,7 +3,7 @@ package ca.uhn.fhir.jpa.term; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao; import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; @@ -550,14 +550,6 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { assertEquals("D1V", concept.getDesignation().get(0).getValue()); } - @Test - public void testReindexTerminology() { - IIdType id = createCodeSystem(); - - assertThat(mySystemDao.markAllResourcesForReindexing(), greaterThan(0)); - - assertThat(mySystemDao.performReindexingPass(100), greaterThan(0)); - } @Test public void testStoreCodeSystemInvalidCyclicLoop() { diff --git a/hapi-fhir-jpaserver-base/src/test/resources/dstu2/createdeletebundle.json b/hapi-fhir-jpaserver-base/src/test/resources/dstu2/createdeletebundle.json new file mode 100644 index 00000000000..acd21119b49 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/dstu2/createdeletebundle.json @@ -0,0 +1,438 @@ + { + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "fullUrl": "Organization/OrgSJHMC", + "resource": { + "resourceType": "Organization", + "id": "OrgSJHMC", + "identifier": [ + { + "system": "http://www.foo.com/fhir/OrganizationIdentifier", + "value": "SJHMC" + } + ], + "name": "SJHMC" + }, + "request": { + "method": "PUT", + "url": "Organization/OrgSJHMC" + } + }, + { + "fullUrl": "Binary/BinaryQ4564699444", + "resource": { + "resourceType": "Binary", + "id": "BinaryQ4564699444", + "contentType": "text/plain", + "content": "TVNIfF5+XCZ8SE5BTXxTSkhNQ3xITkFNfFNKSE1DfDIwMTYwMzE0MTEwMzI5fHxPUlVeUjAxfFE0NTY0Njk5NDQ0fFR8Mi40fHx8fHx8ODg1OS8xDVBJRHwxfDk2MzI1OHw5NjMyNTheXl5TSkhNQ19NUk5eTVJOfjEwNjMyNTleXl5BWl9FSUR8fEJvYmFeRmV0dHx8MTk3MTEwMTJ8Rnx8MXwxMjQgVyBUSE9NQVMgUkReXlBIT0VOSVheQVpeODUwMTN8fCg2MDIpNjY2LTU1NTV8KDAwMCkwMDAtMDAwMHwxfFN8Tk9OfDE4NTEzMzQxXl5eU0pITUNfRklOfHx8fDJ8fHwwDVBWMXwxfFB8VE9XOF44VDIyXjAxXlNKSE1DfFJ8fHwwNTc1MzleRmdkZWdeVWduZ3heXl5eXl5TSkhNQ19PUkdfRE9DTlVNXjE2OTk3MTAwNDZ8MDU3NTM5XkZnZGVnXlVnbmd4Xl5eXl5eU0pITUNfT1JHX0RPQ05VTV4xNjk5NzEwMDQ2fHxMVFN8fHx8UkF8fHwwNTc1MzleRmdkZWdeVWduZ3heXl5eXl5TSkhNQ19PUkdfRE9DTlVNXjE2OTk3MTAwNDZ8SXx8RXx8fHx8fHx8fHx8fHx8fHx8fHxTSkhNQ3x8QXx8fDIwMTYwMzEzMDkwMDAwDU9SQ3xSRQ1PQlJ8MXw1Njc0ODMyXkhOQU1fT1JERVJJRHx8U1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fDIwMTYwMzEzMTAzOTAwfHx8fHx8fHx8MTIzNDVeVGVzdEdeRUQgUGh5c2ljaWFufHx8fDAwMDEwU1AyMDE2MDAwMDMzNl5ITkFfQUNDTn40MTY3MzY1OF5ITkFfQUNDTklEfHwyMDE2MDMxMzEwNTg1MHx8QVB8Rnx8MXx8fHx8JlJpZ3BtZ2EmTnRnYWFpLUNDJiYmQXBwbGljYXRpb24gU3lzIEFuYWx5c3QgSUkgLSBMfHwxMjM0NV5UZXN0R15FRCBQaHlzaWNpYW4NT0JYfDF8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwyfFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fHx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8M3xUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHx8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDR8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHw1fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDZ8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHw3fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgQ29sbGVjdGVkIERhdGUvVGltZSAgICBSZWNlaXZlZCBEYXRlL1RpbWUgICAgICAgICAgICAgICBBY2Nlc3Npb24gTnVtYmVyfHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHw4fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgMDMvMTMvMjAxNiAxMDozOTowMCAgICAwMy8xMy8yMDE2IDEwOjUyOjUwICAgICAgICAgICAgICAxMC1TUC0xNy0wMDAzMzZ8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDl8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICBNU1QgICAgICAgICAgICAgICAgICAgIE1TVHx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MTB8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwxMXxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIERpYWdub3Npc3x8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MTJ8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICAxLTMuICBMdW5nLCBsZWZ0IHVwcGVyIGxvYmUsIENUIGd1aWRlZCBiaW9wc2llcyB3aXRoIHRvdWNoIHByZXBhcmF0aW9uOnx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MTN8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICAgIC0gUG9vcmx5IGRpZmZlcmVudGlhdGVkIG5vbi1zbWFsbCBjZWxsIGNhcmNpbm9tYSwgcGVuZGluZyBzcGVjaWFsIHN0YWluc3x8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MTR8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwxNXxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIE50Z2FhaS1DQyBSaWdwbWdhLCBBcHBsaWNhdGlvbiBTeXMgQW5hbHlzdCBJSSAtIEx8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDE2fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgKEVsZWN0cm9uaWNhbGx5IHNpZ25lZCl8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDE3fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgVmVyaWZpZWQ6IDAzLzEzLzIwMTYgMTA6NTh8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDE4fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgTlIgL05SfHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwxOXxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHx8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDIwfFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgQ2xpbmljYWwgSW5mb3JtYXRpb258fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDIxfFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgUHJlLW9wIGRpYWdub3NpczogVHJhbnNwbGFudHx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MjJ8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICBQcm9jZWR1cmU6IEJpb3BzeXx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MjN8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICBQb3N0LW9wIGRpYWdub3NpczogTi9BfHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwyNHxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIENsaW5pY2FsIEhpc3Rvcnk6IEx1bmcgdHJhbnNwbGFudHx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MjV8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwyNnxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBTcGVjaW1lbiBTdWJtaXR0ZWR8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDI3fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgTFVORywgVFJOU0JSIEJYfHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwyOHxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHx8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDI5fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBHcm9zcyBEZXNjcmlwdGlvbnx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MzB8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICAxLiBSZWNlaXZlZCBpbiBmb3JtYWxpbiBsYWJlbGVkIHdpdGggdGhlIHBhdGllbnQncyBuYW1lLCBtZWRpY2FsIHJlY29yZHx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MzF8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICBudW1iZXIgYW5kIGxlZnQgdXBwZXIgbG9iZSBjb3JlIGJpb3BzeSwgaXMgYSBzaW5nbGUgcmVkLXRhbiwgdmFyaWVnYXRlZCx8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDMyfFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgZnJpYWJsZSBzb2Z0IHRpc3N1ZSBjb3JlLCAwLjkgY20uICBUaGUgc3BlY2ltZW4gaXMgZW50aXJlbHkgc3VibWl0dGVkIGlufHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwzM3xUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIGNhc3NldHRlIDFBLnx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8MzR8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwzNXxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIDIuIFJlY2VpdmVkIGluIGZvcm1hbGluIGxhYmVsZWQgd2l0aCB0aGUgcGF0aWVudCdzIG5hbWUsIG1lZGljYWwgcmVjb3JkfHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwzNnxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIG51bWJlciBhbmQgbGVmdCB1cHBlciBsb2JlIGNvcmUgYmlvcHN5LCBhcmUgdHdvIHBhbGUgZ3JheSwgZnJpYWJsZSBzb2Z0fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHwzN3xUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIHRpc3N1ZSBjb3JlcywgMC40LCBhbmQgMS4wIGNtLiAgVGhlIHNwZWNpbWVuIGlzIGVudGlyZWx5IHN1Ym1pdHRlZCBpbnx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8Mzh8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICBjYXNzZXR0ZSAyQS4gIEEgcXVpY2sgc3RhaW4gaXMgcHJlcGFyZWQgYW5kIGV4YW1pbmVkLnx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8Mzl8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHw0MHxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIFF1aWNrIFN0YWluIEludGVycHJldGF0aW9uOiBbSk1FXXx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8NDF8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICAgIFFTMTogUG9zaXRpdmUufHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHw0MnxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHx8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDQzfFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgMy4gIFJlY2VpdmVkIGluIGZvcm1hbGluIGxhYmVsZWQgd2l0aCB0aGUgcGF0aWVudCdzIG5hbWUsIG1lZGljYWwgcmVjb3JkfHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHw0NHxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIG51bWJlciBhbmQgbGVmdCB1cHBlciBsb2JlIGNvcmUgYmlvcHN5LCBpcyBhIHNpbmdsZSByZWQtdGFuIHNvZnQgdGlzc3VlfHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHw0NXxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIGNvcmUsIDAuNSBjbS4gIFRoZSBzcGVjaW1lbiBpcyBlbnRpcmVseSBzdWJtaXR0ZWQgaW4gY2Fzc2V0dGUgM0EufHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHw0NnxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHx8fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDQ3fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBNaWNyb3Njb3BpYyBEZXNjcmlwdGlvbnx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8NDh8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8ICAgICBNaWNyb3Njb3BpYyBleGFtaW5hdGlvbiBwZXJmb3JtZWQgb24gYWxsIGhpc3RvbG9naWMgc2VjdGlvbnMuQW5kIGFsc28gZm91bmQgaW5jaWRlbnRhbCBsdW5nIG5vZHVsZS58fHx8fHxGfHx8MjAxNjAzMTMxMDU4NTANT0JYfDQ5fFRYfFNVUkdQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnReXlNQQVRIXlN1cmdpY2FsIFBhdGhvbG9neSBSZXBvcnR8fHx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA1PQlh8NTB8VFh8U1VSR1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydF5eU1BBVEheU3VyZ2ljYWwgUGF0aG9sb2d5IFJlcG9ydHx8fHx8fHx8Rnx8fDIwMTYwMzEzMTA1ODUwDU9CWHw1MXxUWHxTVVJHUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0Xl5TUEFUSF5TdXJnaWNhbCBQYXRob2xvZ3kgUmVwb3J0fHwgICAgIFpaVEVTVCwgVFJBTlNQTEFOVCAgICAgICAgICAgICAgICAgICAgIDE1MTYwNTAoU0pIKXx8fHx8fEZ8fHwyMDE2MDMxMzEwNTg1MA==" + }, + "request": { + "method": "POST", + "url": "Binary" + } + }, + { + "fullUrl": "Patient/Patient1063259", + "resource": { + "resourceType": "Patient", + "id": "Patient1063259", + "extension": [ + { + "url": "http://www.foo.com/fhir/extensions/CurrentWorkFlow", + "valueString": "pulmonary" + } + ], + "identifier": [ + { + "system": "http://www.foo.com/fhir/identifier-type/EnterpriseId", + "value": "1063259" + }, + { + "type": { + "coding": [ + { + "system": "http://www.foo.com/Patient/UnknownCode", + "code": "MRN", + "display": "MRN" + } + ] + }, + "system": "http://www.foo.com/fhir/identifier-type/MR", + "value": "963258" + }, + { + "type": { + "coding": [ + { + "system": "http://www.foo.com/Patient/UnknownCode", + "code": "MRN", + "display": "MRN" + } + ] + }, + "system": "http://www.foo.com/fhir/identifier-type/MRN", + "value": "963258" + }, + { + "system": "http://www.foo.com/fhir/identifier-type/", + "value": "1063259" + }, + { + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/identifier-type", + "code": "AN", + "display": "Account number" + } + ] + }, + "system": "http://www.foo.com/fhir/identifier-type/AN", + "value": "18513341" + } + ], + "name": [ + { + "use": "usual", + "family": [ + "Boba" + ], + "given": [ + "Fett" + ] + } + ], + "telecom": [ + { + "system": "phone", + "value": "(602)666-5555", + "use": "home" + }, + { + "system": "phone", + "value": "(000)000-0000", + "use": "work" + } + ], + "gender": "female", + "birthDate": "1971-10-12", + "address": [ + { + "line": [ + "124 W THOMAS RD" + ], + "city": "PHOENIX", + "state": "AZ", + "postalCode": "85013" + } + ], + "maritalStatus": { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/MaritalStatus", + "code": "S", + "display": "Never Married" + } + ] + }, + "multipleBirthInteger": 0, + "communication": [ + { + "language": { + "coding": [ + { + "code": "1" + } + ] + }, + "preferred": true + } + ], + "active": false + }, + "request": { + "method": "PUT", + "url": "Patient/Patient1063259" + } + }, + { + "fullUrl": "Practitioner/Pract057539", + "resource": { + "resourceType": "Practitioner", + "id": "Pract057539", + "identifier": [ + { + "use": "official", + "system": "http://www.foo.com/fhir/PractitionerIdentifier", + "value": "057539" + } + ], + "name": { + "family": [ + "Fgdeg" + ], + "given": [ + "Ugngx" + ] + }, + "gender": "unknown", + "practitionerRole": [ + { + "role": { + "coding": [ + { + "system": "http://hl7.org/fhir/practitioner-role", + "code": "doctor", + "display": "Doctor" + } + ] + } + } + ], + "communication": [ + { + "coding": [ + { + "code": "1" + } + ] + } + ] + }, + "request": { + "method": "PUT", + "url": "Practitioner/Pract057539" + } + }, + { + "fullUrl": "Practitioner/Pract12345", + "resource": { + "resourceType": "Practitioner", + "id": "Pract12345", + "identifier": [ + { + "use": "official", + "system": "http://www.foo.com/fhir/PractitionerIdentifier", + "value": "12345" + } + ], + "name": { + "family": [ + "TestG" + ], + "given": [ + "ED Physician" + ] + }, + "gender": "unknown", + "practitionerRole": [ + { + "role": { + "coding": [ + { + "system": "http://hl7.org/fhir/practitioner-role", + "code": "doctor", + "display": "Doctor" + } + ] + } + } + ], + "communication": [ + { + "coding": [ + { + "code": "1" + } + ] + } + ] + }, + "request": { + "method": "PUT", + "url": "Practitioner/Pract12345" + } + }, + { + "fullUrl": "Location/LocTOW8.8T22.01", + "resource": { + "resourceType": "Location", + "id": "LocTOW8.8T22.01", + "identifier": [ + { + "system": "http://www.foo.com/fhir/LocationIdentifier", + "value": "TOW8.8T22.01" + } + ], + "name": "SJHMC" + }, + "request": { + "method": "PUT", + "url": "Location/LocTOW8.8T22.01" + } + }, + { + "fullUrl": "Observation/ObxSURGPATH0", + "resource": { + "resourceType": "Observation", + "id": "ObxSURGPATH0", + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://www.foo.com/fhir/", + "code": "LOOKUP", + "display": "LOOKUP" + } + ] + }, + "value": "_SURGPATH" + } + ], + "status": "final", + "code": { + "coding": [ + { + "system": "http://www.foo.com/Observation/UnknownCode", + "code": "SURGPATH", + "display": "SURGPATH" + } + ] + }, + "subject": { + "reference": "Patient/Patient1063259", + "display": "Boba Fett " + }, + "effectiveDateTime": "2016-03-13T15:58:50Z", + "issued": "2016-03-13T15:58:50Z", + "performer": [ + { + "reference": "Practitioner/Pract12345" + } + ], + "valueString": "\\\\n\\\\n\\\\n\\\\n Surgical Pathology Report\\\\n\\\\n Collected Date/Time Received Date/Time Accession Number\\\\n 03/13/2016 10:39:00 03/13/2016 10:52:50 10-SP-17-000336\\\\n MST MST\\\\n\\\\n Diagnosis\\\\n 1-3. Lung, left upper lobe, CT guided biopsies with touch preparation:\\\\n - Poorly differentiated non-small cell carcinoma, pending special stains\\\\n\\\\n Ntgaai-CC Rigpmga, Application Sys Analyst II - L\\\\n (Electronically signed)\\\\n Verified: 03/13/2016 10:58\\\\n NR /NR\\\\n\\\\n Clinical Information\\\\n Pre-op diagnosis: Transplant\\\\n Procedure: Biopsy\\\\n Post-op diagnosis: N/A\\\\n Clinical History: Lung transplant\\\\n\\\\n Specimen Submitted\\\\n LUNG, TRNSBR BX\\\\n\\\\n Gross Description\\\\n 1. Received in formalin labeled with the patient's name, medical record\\\\n number and left upper lobe core biopsy, is a single red-tan, variegated,\\\\n friable soft tissue core, 0.9 cm. The specimen is entirely submitted in\\\\n cassette 1A.\\\\n\\\\n 2. Received in formalin labeled with the patient's name, medical record\\\\n number and left upper lobe core biopsy, are two pale gray, friable soft\\\\n tissue cores, 0.4, and 1.0 cm. The specimen is entirely submitted in\\\\n cassette 2A. A quick stain is prepared and examined.\\\\n\\\\n Quick Stain Interpretation: [JME]\\\\n QS1: Positive.\\\\n\\\\n 3. Received in formalin labeled with the patient's name, medical record\\\\n number and left upper lobe core biopsy, is a single red-tan soft tissue\\\\n core, 0.5 cm. The specimen is entirely submitted in cassette 3A.\\\\n\\\\n Microscopic Description\\\\n Microscopic examination performed on all histologic sections.And also found incidental lung nodule.\\\\n\\\\n\\\\n ZZTEST, TRANSPLANT 1516050(SJH)", + "device": { + "display": "EMR" + } + }, + "request": { + "method": "PUT", + "url": "Observation/ObxSURGPATH0" + } + }, + { + "fullUrl": "DiagnosticReport/ReportSPATH", + "resource": { + "resourceType": "DiagnosticReport", + "id": "ReportSPATH", + "identifier": [ + { + "system": "http://www.foo.com/fhir/DiagnosticReport", + "value": "5674832" + } + ], + "status": "final", + "category": { + "coding": [ + { + "system": "http://www.foo.com/DiagnosticReport/UnknownCode", + "code": "00010SP20160000336", + "display": "00010SP20160000336" + } + ], + "text": "00010SP20160000336" + }, + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "SPATH", + "display": "Surgical Pathology Report" + } + ], + "text": "Surgical Pathology Report" + }, + "subject": { + "reference": "Patient/Patient1063259", + "display": "Boba Fett " + }, + "effectiveDateTime": "2016-03-13T15:39:00Z", + "issued": "2016-03-13T15:58:50Z", + "performer": { + "reference": "Practitioner/Pract12345" + }, + "request": [ + { + "reference": "DiagnosticOrder/ORCSPATH" + } + ], + "result": [ + { + "reference": "Observation/ObxSURGPATH0" + } + ] + }, + "request": { + "method": "PUT", + "url": "DiagnosticReport/ReportSPATH" + } + }, + { + "fullUrl": "DiagnosticOrder/ORCSPATH", + "resource": { + "resourceType": "DiagnosticOrder", + "id": "ORCSPATH", + "extension": [ + { + "url": "http://www.foo.com/fhir/extensions/ModalityType", + "valueString": "AP" + }, + { + "url": "http://www.foo.com/fhir/extensions/SendingApplication", + "valueString": "EPIC" + } + ], + "subject": { + "reference": "Patient/Patient1063259" + }, + "orderer": { + "reference": "Practitioner/Pract12345" + }, + "identifier": [ + { + "system": "http://www.foo.com/fhir/DiagnosticOrder", + "value": "EPIC_5674832" + } + ], + "status": "completed", + "event": [ + { + "status": "in-progress", + "dateTime": "2016-03-13T15:39:00Z" + } + ], + "item": [ + { + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "SPATH", + "display": "Surgical Pathology Report" + } + ], + "text": "Surgical Pathology Report" + } + } + ] + }, + "request": { + "method": "PUT", + "url": "DiagnosticOrder/ORCEPIC5674832" + } + } + ] +} diff --git a/hapi-fhir-jpaserver-base/src/test/resources/dstu3/Reilly_Libby_73.json.gz b/hapi-fhir-jpaserver-base/src/test/resources/dstu3/Reilly_Libby_73.json.gz new file mode 100644 index 00000000000..17ab03b36ac Binary files /dev/null and b/hapi-fhir-jpaserver-base/src/test/resources/dstu3/Reilly_Libby_73.json.gz differ diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/Loinc.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincTable/Loinc.csv similarity index 100% rename from hapi-fhir-jpaserver-base/src/test/resources/loinc/Loinc.csv rename to hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincTable/Loinc.csv diff --git a/hapi-fhir-jpaserver-base/src/test/resources/r4/createdeletebundle.json b/hapi-fhir-jpaserver-base/src/test/resources/r4/createdeletebundle.json new file mode 100644 index 00000000000..abe36b0a8a9 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/r4/createdeletebundle.json @@ -0,0 +1,37 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "fullUrl": "Patient/Patient1063259", + "resource": { + "resourceType": "Patient", + "id": "Patient1063259", + "identifier": [ + { + "system": "http://www.foo.com/fhir/identifier-type/EnterpriseId", + "value": "1063259" + } + ] + }, + "request": { + "method": "PUT", + "url": "Patient/Patient1063259" + } + }, + { + "fullUrl": "DiagnosticReport/ReportSPATH", + "resource": { + "resourceType": "DiagnosticReport", + "id": "ReportSPATH", + "subject": { + "reference": "Patient/Patient1063259" + } + }, + "request": { + "method": "PUT", + "url": "DiagnosticReport/ReportSPATH" + } + } + ] +} diff --git a/hapi-fhir-jpaserver-elasticsearch/pom.xml b/hapi-fhir-jpaserver-elasticsearch/pom.xml index a7c5192fb89..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-SNAPSHOT + 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 34e60121ae5..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-SNAPSHOT + 3.7.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java index 8a2b429b2f7..9ef7ae73549 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java @@ -5,12 +5,17 @@ import java.util.Properties; import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory; import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect; import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.lang3.time.DateUtils; import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.orm.jpa.JpaTransactionManager; @@ -34,13 +39,18 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { /** * Configure FHIR properties around the the JPA server via this bean */ - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setAllowMultipleDelete(true); return retVal; } + @Bean + public ModelConfig modelConfig() { + return daoConfig().getModelConfig(); + } + /** * The following bean configures the database connection. The 'url' property value of "jdbc:derby:directory:jpaserver_derby_files;create=true" indicates that the server should save resources in a * directory called "jpaserver_derby_files". @@ -58,7 +68,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("HAPI_PU"); @@ -114,11 +124,10 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { return retVal; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); return retVal; } - } diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java index 6d71dedc40e..844db7f40db 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java @@ -32,7 +32,7 @@ public class FhirServerConfigDstu2 extends BaseJavaConfigDstu2 { /** * Configure FHIR properties around the the JPA server via this bean */ - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -59,7 +59,7 @@ public class FhirServerConfigDstu2 extends BaseJavaConfigDstu2 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("HAPI_PU"); @@ -115,7 +115,7 @@ public class FhirServerConfigDstu2 extends BaseJavaConfigDstu2 { return retVal; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); 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-example/src/main/java/ca/uhn/fhir/jpa/demo/elasticsearch/FhirServerConfig.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/elasticsearch/FhirServerConfig.java index f9e6e502c43..3ea02f8b438 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/elasticsearch/FhirServerConfig.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/elasticsearch/FhirServerConfig.java @@ -30,7 +30,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { /** * Configure FHIR properties around the the JPA server via this bean */ - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setAllowMultipleDelete(true); @@ -54,7 +54,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("HAPI_PU"); @@ -100,7 +100,7 @@ public class FhirServerConfig extends BaseJavaConfigDstu3 { return retVal; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); diff --git a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/ExampleServerIT.java b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/ExampleServerIT.java index a151f6ca6cb..2d85019ec3d 100644 --- a/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/ExampleServerIT.java +++ b/hapi-fhir-jpaserver-example/src/test/java/ca/uhn/fhir/jpa/demo/ExampleServerIT.java @@ -76,6 +76,7 @@ public class ExampleServerIT { ourClient = ourCtx.newRestfulGenericClient(ourServerBase); ourClient.registerInterceptor(new LoggingInterceptor(true)); + } public static void main(String[] theArgs) throws Exception { diff --git a/hapi-fhir-jpaserver-migrate/pom.xml b/hapi-fhir-jpaserver-migrate/pom.xml index a804fc80a9d..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-SNAPSHOT + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -31,6 +31,10 @@ org.springframework spring-jdbc + + org.apache.commons + commons-dbcp2 + @@ -45,11 +49,6 @@ derby test - - org.apache.commons - commons-dbcp2 - test - junit junit diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/DriverTypeEnum.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/DriverTypeEnum.java index 8e5cba77e1d..d15e4bc6027 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/DriverTypeEnum.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/DriverTypeEnum.java @@ -1,14 +1,13 @@ package ca.uhn.fhir.jpa.migrate; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.SimpleDriverDataSource; -import org.springframework.jdbc.datasource.SingleConnectionDataSource; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.TransactionTemplate; @@ -17,7 +16,6 @@ import javax.sql.DataSource; import java.sql.Connection; import java.sql.Driver; import java.sql.SQLException; -import java.util.Properties; /*- * #%L @@ -76,20 +74,17 @@ public enum DriverTypeEnum { throw new InternalErrorException("Unable to find driver class: " + myDriverClassName, e); } - SingleConnectionDataSource dataSource = new SingleConnectionDataSource(){ + BasicDataSource dataSource = new BasicDataSource(){ @Override - protected Connection getConnectionFromDriver(Properties props) throws SQLException { - Connection connect = driver.connect(theUrl, props); - assert connect != null; - return connect; + public Connection getConnection() throws SQLException { + ourLog.debug("Creating new DB connection"); + return super.getConnection(); } }; - dataSource.setAutoCommit(false); dataSource.setDriverClassName(myDriverClassName); dataSource.setUrl(theUrl); dataSource.setUsername(theUsername); dataSource.setPassword(thePassword); - dataSource.setSuppressClose(true); DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); 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 4a23cb61366..7cbb219d533 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 @@ -21,8 +21,11 @@ package ca.uhn.fhir.jpa.migrate; */ import ca.uhn.fhir.jpa.migrate.taskdef.BaseTableColumnTypeTask; -import ca.uhn.fhir.jpa.migrate.taskdef.BaseTableTask; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; +import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.ColumnMapRowMapper; @@ -45,56 +48,56 @@ public class JdbcUtils { public static Set getIndexNames(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName) throws SQLException { DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); - Connection connection = dataSource.getConnection(); - return theConnectionProperties.getTxTemplate().execute(t -> { - DatabaseMetaData metadata; - try { - metadata = connection.getMetaData(); - ResultSet indexes = metadata.getIndexInfo(null, null, theTableName, false, true); + try (Connection connection = dataSource.getConnection()) { + return theConnectionProperties.getTxTemplate().execute(t -> { + DatabaseMetaData metadata; + try { + metadata = connection.getMetaData(); + ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), theTableName, false, true); - Set indexNames = new HashSet<>(); - while (indexes.next()) { + Set indexNames = new HashSet<>(); + while (indexes.next()) { - ourLog.debug("*** Next index: {}", new ColumnMapRowMapper().mapRow(indexes, 0)); + ourLog.debug("*** Next index: {}", new ColumnMapRowMapper().mapRow(indexes, 0)); - String indexName = indexes.getString("INDEX_NAME"); - indexName = toUpperCase(indexName, Locale.US); - indexNames.add(indexName); + String indexName = indexes.getString("INDEX_NAME"); + indexName = toUpperCase(indexName, Locale.US); + indexNames.add(indexName); + } + + return indexNames; + } catch (SQLException e) { + throw new InternalErrorException(e); } - - return indexNames; - } catch (SQLException e) { - throw new InternalErrorException(e); - } - }); - + }); + } } @SuppressWarnings("ConstantConditions") public static boolean isIndexUnique(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName, String theIndexName) throws SQLException { DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); - Connection connection = dataSource.getConnection(); - return theConnectionProperties.getTxTemplate().execute(t -> { - DatabaseMetaData metadata; - try { - metadata = connection.getMetaData(); - ResultSet indexes = metadata.getIndexInfo(null, null, theTableName, false, false); + try (Connection connection = dataSource.getConnection()) { + return theConnectionProperties.getTxTemplate().execute(t -> { + DatabaseMetaData metadata; + try { + metadata = connection.getMetaData(); + ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), theTableName, false, false); - while (indexes.next()) { - String indexName = indexes.getString("INDEX_NAME"); - if (theIndexName.equalsIgnoreCase(indexName)) { - boolean nonUnique = indexes.getBoolean("NON_UNIQUE"); - return !nonUnique; + while (indexes.next()) { + String indexName = indexes.getString("INDEX_NAME"); + if (theIndexName.equalsIgnoreCase(indexName)) { + boolean nonUnique = indexes.getBoolean("NON_UNIQUE"); + return !nonUnique; + } } + + } catch (SQLException e) { + throw new InternalErrorException(e); } - } catch (SQLException e) { - throw new InternalErrorException(e); - } - - throw new InternalErrorException("Can't find index: " + theIndexName + " on table " + theTableName); - }); - + throw new InternalErrorException("Can't find index: " + theIndexName + " on table " + theTableName); + }); + } } /** @@ -107,7 +110,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()) { @@ -125,7 +130,9 @@ public class JdbcUtils { switch (dataType) { case Types.VARCHAR: return BaseTableColumnTypeTask.ColumnTypeEnum.STRING.getDescriptor(length); + case Types.NUMERIC: case Types.BIGINT: + case Types.DECIMAL: return BaseTableColumnTypeTask.ColumnTypeEnum.LONG.getDescriptor(null); case Types.INTEGER: return BaseTableColumnTypeTask.ColumnTypeEnum.INT.getDescriptor(null); @@ -133,7 +140,7 @@ public class JdbcUtils { case Types.TIMESTAMP_WITH_TIMEZONE: return BaseTableColumnTypeTask.ColumnTypeEnum.DATE_TIMESTAMP.getDescriptor(null); default: - throw new IllegalArgumentException("Don't know how to handle datatype: " + dataType); + throw new IllegalArgumentException("Don't know how to handle datatype " + dataType + " for column " + theColumnName + " on table " + theTableName); } } @@ -153,130 +160,169 @@ public class JdbcUtils { */ public static Set getForeignKeys(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName, String theForeignTable) throws SQLException { DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); - Connection connection = dataSource.getConnection(); - return theConnectionProperties.getTxTemplate().execute(t -> { - DatabaseMetaData metadata; - try { - metadata = connection.getMetaData(); - ResultSet indexes = metadata.getCrossReference(null, null, theTableName, null, null, theForeignTable); + try (Connection connection = dataSource.getConnection()) { + return theConnectionProperties.getTxTemplate().execute(t -> { + DatabaseMetaData metadata; + try { + metadata = connection.getMetaData(); + ResultSet indexes = metadata.getCrossReference(connection.getCatalog(), connection.getSchema(), theTableName, connection.getCatalog(), connection.getSchema(), theForeignTable); - Set columnNames = new HashSet<>(); - while (indexes.next()) { - String tableName = toUpperCase(indexes.getString("PKTABLE_NAME"), Locale.US); - if (!theTableName.equalsIgnoreCase(tableName)) { - continue; - } - tableName = toUpperCase(indexes.getString("FKTABLE_NAME"), Locale.US); - if (!theForeignTable.equalsIgnoreCase(tableName)) { - continue; + Set columnNames = new HashSet<>(); + while (indexes.next()) { + String tableName = toUpperCase(indexes.getString("PKTABLE_NAME"), Locale.US); + if (!theTableName.equalsIgnoreCase(tableName)) { + continue; + } + tableName = toUpperCase(indexes.getString("FKTABLE_NAME"), Locale.US); + if (!theForeignTable.equalsIgnoreCase(tableName)) { + continue; + } + + String fkName = indexes.getString("FK_NAME"); + fkName = toUpperCase(fkName, Locale.US); + columnNames.add(fkName); } - String fkName = indexes.getString("FK_NAME"); - fkName = toUpperCase(fkName, Locale.US); - columnNames.add(fkName); + return columnNames; + } catch (SQLException e) { + throw new InternalErrorException(e); } - - return columnNames; - } catch (SQLException e) { - throw new InternalErrorException(e); - } - }); + }); + } } - /** - * Retrieve all index names - */ + /** + * Retrieve all index names + */ public static Set getColumnNames(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName) throws SQLException { DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); - Connection connection = dataSource.getConnection(); - return theConnectionProperties.getTxTemplate().execute(t -> { - DatabaseMetaData metadata; - try { - metadata = connection.getMetaData(); - ResultSet indexes = metadata.getColumns(null, null, null, null); + try (Connection connection = dataSource.getConnection()) { + return theConnectionProperties.getTxTemplate().execute(t -> { + DatabaseMetaData metadata; + try { + metadata = connection.getMetaData(); + ResultSet indexes = metadata.getColumns(connection.getCatalog(), connection.getSchema(), theTableName, null); - Set columnNames = new HashSet<>(); - while (indexes.next()) { - String tableName = toUpperCase(indexes.getString("TABLE_NAME"), Locale.US); - if (!theTableName.equalsIgnoreCase(tableName)) { - continue; + Set columnNames = new HashSet<>(); + while (indexes.next()) { + String tableName = toUpperCase(indexes.getString("TABLE_NAME"), Locale.US); + if (!theTableName.equalsIgnoreCase(tableName)) { + continue; + } + + String columnName = indexes.getString("COLUMN_NAME"); + columnName = toUpperCase(columnName, Locale.US); + columnNames.add(columnName); } - String columnName = indexes.getString("COLUMN_NAME"); - columnName = toUpperCase(columnName, Locale.US); - columnNames.add(columnName); + return columnNames; + } catch (SQLException e) { + throw new InternalErrorException(e); } + }); + } + } - return columnNames; - } catch (SQLException e) { - throw new InternalErrorException(e); - } - }); + public static Set getSequenceNames(DriverTypeEnum.ConnectionProperties theConnectionProperties) throws SQLException { + DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); + try (Connection connection = dataSource.getConnection()) { + return theConnectionProperties.getTxTemplate().execute(t -> { + try { + DialectResolver dialectResolver = new StandardDialectResolver(); + Dialect dialect = dialectResolver.resolveDialect(new DatabaseMetaDataDialectResolutionInfoAdapter(connection.getMetaData())); + Set sequenceNames = new HashSet<>(); + if (dialect.supportsSequences()) { + String sql = dialect.getQuerySequencesString(); + if (sql != null) { + + Statement statement = null; + ResultSet rs = null; + try { + statement = connection.createStatement(); + rs = statement.executeQuery(sql); + + while (rs.next()) { + sequenceNames.add(rs.getString(1).toUpperCase()); + } + } finally { + if (rs != null) rs.close(); + if (statement != null) statement.close(); + } + + } + } + return sequenceNames; + } catch (SQLException e ) { + throw new InternalErrorException(e); + } + }); + } } public static Set getTableNames(DriverTypeEnum.ConnectionProperties theConnectionProperties) throws SQLException { DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); - Connection connection = dataSource.getConnection(); - return theConnectionProperties.getTxTemplate().execute(t -> { - DatabaseMetaData metadata; - try { - metadata = connection.getMetaData(); - ResultSet tables = metadata.getTables(null, null, null, null); + try (Connection connection = dataSource.getConnection()) { + return theConnectionProperties.getTxTemplate().execute(t -> { + DatabaseMetaData metadata; + try { + metadata = connection.getMetaData(); + ResultSet tables = metadata.getTables(connection.getCatalog(), connection.getSchema(), null, null); - Set columnNames = new HashSet<>(); - while (tables.next()) { - String tableName = tables.getString("TABLE_NAME"); - tableName = toUpperCase(tableName, Locale.US); + Set columnNames = new HashSet<>(); + while (tables.next()) { + String tableName = tables.getString("TABLE_NAME"); + tableName = toUpperCase(tableName, Locale.US); - String tableType = tables.getString("TABLE_TYPE"); - if ("SYSTEM TABLE".equalsIgnoreCase(tableType)) { - continue; + String tableType = tables.getString("TABLE_TYPE"); + if ("SYSTEM TABLE".equalsIgnoreCase(tableType)) { + continue; + } + + columnNames.add(tableName); } - columnNames.add(tableName); + return columnNames; + } catch (SQLException e) { + throw new InternalErrorException(e); } - - return columnNames; - } catch (SQLException e) { - throw new InternalErrorException(e); - } - }); + }); + } } public static boolean isColumnNullable(DriverTypeEnum.ConnectionProperties theConnectionProperties, String theTableName, String theColumnName) throws SQLException { DataSource dataSource = Objects.requireNonNull(theConnectionProperties.getDataSource()); - Connection connection = dataSource.getConnection(); - //noinspection ConstantConditions - return theConnectionProperties.getTxTemplate().execute(t -> { - DatabaseMetaData metadata; - try { - metadata = connection.getMetaData(); - ResultSet tables = metadata.getColumns(null, null, null, null); + try (Connection connection = dataSource.getConnection()) { + //noinspection ConstantConditions + return theConnectionProperties.getTxTemplate().execute(t -> { + DatabaseMetaData metadata; + try { + metadata = connection.getMetaData(); + ResultSet tables = metadata.getColumns(connection.getCatalog(), connection.getSchema(), theTableName, theColumnName); - while (tables.next()) { - String tableName = toUpperCase(tables.getString("TABLE_NAME"), Locale.US); - if (!theTableName.equalsIgnoreCase(tableName)) { - continue; - } + while (tables.next()) { + String tableName = toUpperCase(tables.getString("TABLE_NAME"), Locale.US); + if (!theTableName.equalsIgnoreCase(tableName)) { + continue; + } - if (theColumnName.equalsIgnoreCase(tables.getString("COLUMN_NAME"))) { - String nullable = tables.getString("IS_NULLABLE"); - if ("YES".equalsIgnoreCase(nullable)) { - return true; - } else if ("NO".equalsIgnoreCase(nullable)) { - return false; - } else { - throw new IllegalStateException("Unknown nullable: " + nullable); + if (theColumnName.equalsIgnoreCase(tables.getString("COLUMN_NAME"))) { + String nullable = tables.getString("IS_NULLABLE"); + if ("YES".equalsIgnoreCase(nullable)) { + return true; + } else if ("NO".equalsIgnoreCase(nullable)) { + return false; + } else { + throw new IllegalStateException("Unknown nullable: " + nullable); + } } } + + throw new IllegalStateException("Did not find column " + theColumnName); + } catch (SQLException e) { + throw new InternalErrorException(e); } - - throw new IllegalStateException("Did not find column " + theColumnName); - } catch (SQLException e) { - throw new InternalErrorException(e); - } - }); - + }); + } } } 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 c12d773dfa7..90b41445de6 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 @@ -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,11 +87,36 @@ 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); + } + + } + + public void addTasks(List> theTasks) { + theTasks.forEach(this::addTask); } } 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..5c8e611dd1b 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 @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.migrate.taskdef; */ import ca.uhn.fhir.jpa.migrate.JdbcUtils; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,15 +41,33 @@ public class AddColumnTask extends BaseTableColumnTypeTask { return; } + String typeStatement = getTypeStatement(); + + 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() + " " + typeStatement; + break; + case MSSQL_2012: + case ORACLE_12C: + sql = "alter table " + getTableName() + " add " + getColumnName() + " " + typeStatement; + break; + } + + ourLog.info("Adding column {} of type {} to table {}", getColumnName(), getSqlType(), getTableName()); + executeSql(getTableName(), sql); + } + + public String getTypeStatement() { String type = getSqlType(); String nullable = getSqlNotNull(); if (isNullable()) { nullable = ""; } - - String sql = "alter table " + getTableName() + " add column " + getColumnName() + " " + type + " " + nullable; - ourLog.info("Adding column {} of type {} to table {}", getColumnName(), type, getTableName()); - executeSql(sql); + return type + " " + nullable; } } 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/AddIdGeneratorTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIdGeneratorTask.java new file mode 100644 index 00000000000..b42bacfb67f --- /dev/null +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIdGeneratorTask.java @@ -0,0 +1,91 @@ +package ca.uhn.fhir.jpa.migrate.taskdef; + +/*- + * #%L + * HAPI FHIR JPA Server - Migration + * %% + * 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.migrate.JdbcUtils; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.util.Set; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class AddIdGeneratorTask extends BaseTask { + + private static final Logger ourLog = LoggerFactory.getLogger(AddIdGeneratorTask.class); + private final String myGeneratorName; + + public AddIdGeneratorTask(String theGeneratorName) { + myGeneratorName = theGeneratorName; + } + + @Override + public void validate() { + Validate.notBlank(myGeneratorName); + } + + @Override + public void execute() throws SQLException { + Set tableNames = JdbcUtils.getTableNames(getConnectionProperties()); + String sql = null; + + switch (getDriverType()) { + case MARIADB_10_1: + case MYSQL_5_7: + // These require a separate table + if (!tableNames.contains(myGeneratorName)) { + + String creationSql = "create table " + myGeneratorName + " ( next_val bigint ) engine=InnoDB"; + executeSql(myGeneratorName, creationSql); + + String initSql = "insert into " + myGeneratorName + " values ( 1 )"; + executeSql(myGeneratorName, initSql); + + } + break; + case DERBY_EMBEDDED: + sql = "create sequence " + myGeneratorName + " start with 1 increment by 50"; + break; + case POSTGRES_9_4: + sql = "create sequence " + myGeneratorName + " start 1 increment 50"; + break; + case ORACLE_12C: + sql = "create sequence " + myGeneratorName + " start with 1 increment by 50"; + break; + case MSSQL_2012: + sql = "create sequence " + myGeneratorName + " start with 1 increment by 50"; + break; + } + + if (isNotBlank(sql)) { + if (JdbcUtils.getSequenceNames(getConnectionProperties()).contains(myGeneratorName)) { + ourLog.info("Sequence {} already exists - No action performed", myGeneratorName); + return; + } + + executeSql(myGeneratorName, sql); + } + + } + +} 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/AddTableByColumnTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableByColumnTask.java new file mode 100644 index 00000000000..e332c0e9827 --- /dev/null +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableByColumnTask.java @@ -0,0 +1,91 @@ +package ca.uhn.fhir.jpa.migrate.taskdef; + +/*- + * #%L + * HAPI FHIR JPA Server - Migration + * %% + * 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.migrate.JdbcUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class AddTableByColumnTask extends BaseTableTask { + + private static final Logger ourLog = LoggerFactory.getLogger(AddTableByColumnTask.class); + + private List myAddColumnTasks = new ArrayList<>(); + private String myPkColumn; + + public void addAddColumnTask(AddColumnTask theTask) { + myAddColumnTasks.add(theTask); + } + + public void setPkColumn(String thePkColumn) { + myPkColumn = thePkColumn; + } + + @Override + public void execute() throws SQLException { + + if (JdbcUtils.getTableNames(getConnectionProperties()).contains(getTableName())) { + ourLog.info("Already have table named {} - No action performed", getTableName()); + return; + } + + StringBuilder sb = new StringBuilder(); + sb.append("CREATE TABLE "); + sb.append(getTableName()); + sb.append(" ( "); + + for (AddColumnTask next : myAddColumnTasks) { + next.setDriverType(getDriverType()); + next.setTableName(getTableName()); + next.validate(); + + sb.append(next.getColumnName()); + sb.append(" "); + sb.append(next.getTypeStatement()); + sb.append(", "); + } + + sb.append(" PRIMARY KEY ("); + sb.append(myPkColumn); + sb.append(")"); + + sb.append(" ) "); + + switch (getDriverType()) { + case MARIADB_10_1: + case MYSQL_5_7: + sb.append("engine=InnoDB"); + break; + case DERBY_EMBEDDED: + case POSTGRES_9_4: + case ORACLE_12C: + case MSSQL_2012: + break; + } + + executeSql(getTableName(), sb.toString()); + + } +} diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableRawSqlTask.java similarity index 95% rename from hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableTask.java rename to hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableRawSqlTask.java index 99c6c87ffe2..15a548a240b 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableRawSqlTask.java @@ -31,9 +31,9 @@ import org.springframework.jdbc.core.JdbcTemplate; import java.sql.SQLException; import java.util.*; -public class AddTableTask extends BaseTableTask { +public class AddTableRawSqlTask extends BaseTableTask { - private static final Logger ourLog = LoggerFactory.getLogger(AddTableTask.class); + private static final Logger ourLog = LoggerFactory.getLogger(AddTableRawSqlTask.class); private Map> myDriverToSqls = new HashMap<>(); public void addSql(DriverTypeEnum theDriverType, @Language("SQL") String theSql) { 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..e8f892bdfda 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 @@ -38,11 +38,14 @@ 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; + private List myConditionalOnExistenceOf = new ArrayList<>(); - public ArbitrarySqlTask(String theDescription) { + public ArbitrarySqlTask(String theTableName, String theDescription) { + myTableName = theTableName; myDescription = theDescription; } @@ -67,6 +70,14 @@ public class ArbitrarySqlTask extends BaseTask { } } + for (TableAndColumn next : myConditionalOnExistenceOf) { + String columnType = JdbcUtils.getColumnType(getConnectionProperties(), next.getTable(), next.getColumn()); + if (columnType == null) { + ourLog.info("Table {} does not have column {} - No action performed", next.getTable(), next.getColumn()); + return; + } + } + for (Task next : myTask) { next.execute(); } @@ -81,6 +92,13 @@ public class ArbitrarySqlTask extends BaseTask { myExecuteOnlyIfTableExists = theExecuteOnlyIfTableExists; } + /** + * This task will only execute if the following column exists + */ + public void addExecuteOnlyIfColumnExists(String theTableName, String theColumnName) { + myConditionalOnExistenceOf.add(new TableAndColumn(theTableName, theColumnName)); + } + public enum QueryModeEnum { BATCH_UNTIL_NO_MORE } @@ -104,7 +122,6 @@ public class ArbitrarySqlTask extends BaseTask { @Override public void execute() { if (isDryRun()) { - logDryRunSql(mySql); return; } @@ -128,4 +145,22 @@ public class ArbitrarySqlTask extends BaseTask { } while (rows.size() > 0); } } + + private static class TableAndColumn { + private final String myTable; + private final String myColumn; + + private TableAndColumn(String theTable, String theColumn) { + myTable = theTable; + myColumn = theColumn; + } + + public String getTable() { + return myTable; + } + + public String getColumn() { + return myColumn; + } + } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTypeTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTypeTask.java index 04f88aa1a4e..a863ac73d17 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTypeTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTypeTask.java @@ -115,8 +115,9 @@ public abstract class BaseTableColumnTypeTask extends B return myNullable; } - public void setNullable(boolean theNullable) { + public T setNullable(boolean theNullable) { myNullable = theNullable; + return (T) this; } protected String getSqlNotNull() { @@ -127,8 +128,9 @@ public abstract class BaseTableColumnTypeTask extends B return myColumnLength; } - public void setColumnLength(int theColumnLength) { + public BaseTableColumnTypeTask setColumnLength(int theColumnLength) { myColumnLength = (long) theColumnLength; + return this; } 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..d185eb50b03 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 @@ -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 620792a4754..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 @@ -23,15 +23,21 @@ package ca.uhn.fhir.jpa.migrate.taskdef; import ca.uhn.fhir.util.StopWatch; import com.google.common.collect.ForwardingMap; import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.checkerframework.checker.nullness.compatqual.NullableDecl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.ColumnMapRowMapper; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowCallbackHandler; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.*; import java.util.function.Function; public class CalculateHashesTask extends BaseTableColumnTask { @@ -39,75 +45,147 @@ public class CalculateHashesTask extends BaseTableColumnTask, Long>> myCalculators = new HashMap<>(); + private ThreadPoolExecutor myExecutor; public void setBatchSize(int theBatchSize) { myBatchSize = theBatchSize; } + /** + * Constructor + */ + public CalculateHashesTask() { + super(); + } @Override - public void execute() { + public synchronized void execute() throws SQLException { if (isDryRun()) { return; } - List> rows; - do { - rows = getTxTemplate().execute(t -> { - JdbcTemplate jdbcTemplate = newJdbcTemnplate(); - jdbcTemplate.setMaxRows(myBatchSize); - String sql = "SELECT * FROM " + getTableName() + " WHERE " + getColumnName() + " IS NULL"; - ourLog.info("Finding up to {} rows in {} that requires hashes", myBatchSize, getTableName()); - return jdbcTemplate.queryForList(sql); - }); + initializeExecutor(); + try { - updateRows(rows); - } while (rows.size() > 0); - } + while(true) { + MyRowCallbackHandler rch = new MyRowCallbackHandler(); + getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = newJdbcTemnplate(); + jdbcTemplate.setMaxRows(100000); + String sql = "SELECT * FROM " + getTableName() + " WHERE " + getColumnName() + " IS NULL"; + ourLog.info("Finding up to {} rows in {} that requires hashes", myBatchSize, getTableName()); - private void updateRows(List> theRows) { - StopWatch sw = new StopWatch(); - getTxTemplate().execute(t -> { + jdbcTemplate.query(sql, rch); + rch.done(); - // Loop through rows - assert theRows != null; - for (Map nextRow : theRows) { + return null; + }); - Map newValues = new HashMap<>(); - MandatoryKeyMap nextRowMandatoryKeyMap = new MandatoryKeyMap<>(nextRow); - - // Apply calculators - for (Map.Entry, Long>> nextCalculatorEntry : myCalculators.entrySet()) { - String nextColumn = nextCalculatorEntry.getKey(); - Function, Long> nextCalculator = nextCalculatorEntry.getValue(); - Long value = nextCalculator.apply(nextRowMandatoryKeyMap); - newValues.put(nextColumn, value); + rch.submitNext(); + List> futures = rch.getFutures(); + if (futures.isEmpty()) { + break; } - // Generate update SQL - StringBuilder sqlBuilder = new StringBuilder(); - List arguments = new ArrayList<>(); - sqlBuilder.append("UPDATE "); - sqlBuilder.append(getTableName()); - sqlBuilder.append(" SET "); - for (Map.Entry nextNewValueEntry : newValues.entrySet()) { - if (arguments.size() > 0) { - sqlBuilder.append(", "); + ourLog.info("Waiting for {} tasks to complete", futures.size()); + for (Future next : futures) { + try { + next.get(); + } catch (Exception e) { + throw new SQLException(e); } - sqlBuilder.append(nextNewValueEntry.getKey()).append(" = ?"); - arguments.add(nextNewValueEntry.getValue()); } - sqlBuilder.append(" WHERE SP_ID = ?"); - arguments.add((Long) nextRow.get("SP_ID")); - - // Apply update SQL - newJdbcTemnplate().update(sqlBuilder.toString(), arguments.toArray()); } - return theRows.size(); - }); - ourLog.info("Updated {} rows on {} in {}", theRows.size(), getTableName(), sw.toString()); + } finally { + destroyExecutor(); + } + } + + private void destroyExecutor() { + myExecutor.shutdownNow(); + } + + private void initializeExecutor() { + int maximumPoolSize = Runtime.getRuntime().availableProcessors(); + + LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(maximumPoolSize); + BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() + .namingPattern("worker-" + "-%d") + .daemon(false) + .priority(Thread.NORM_PRIORITY) + .build(); + RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) { + ourLog.info("Note: Executor 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()); + } + }; + myExecutor = new ThreadPoolExecutor( + 1, + maximumPoolSize, + 0L, + TimeUnit.MILLISECONDS, + executorQueue, + threadFactory, + rejectedExecutionHandler); + } + + private Future updateRows(List> theRows) { + Runnable task = () -> { + StopWatch sw = new StopWatch(); + getTxTemplate().execute(t -> { + + // Loop through rows + assert theRows != null; + for (Map nextRow : theRows) { + + Map newValues = new HashMap<>(); + MandatoryKeyMap nextRowMandatoryKeyMap = new MandatoryKeyMap<>(nextRow); + + // Apply calculators + for (Map.Entry, Long>> nextCalculatorEntry : myCalculators.entrySet()) { + String nextColumn = nextCalculatorEntry.getKey(); + Function, Long> nextCalculator = nextCalculatorEntry.getValue(); + Long value = nextCalculator.apply(nextRowMandatoryKeyMap); + newValues.put(nextColumn, value); + } + + // Generate update SQL + StringBuilder sqlBuilder = new StringBuilder(); + List arguments = new ArrayList<>(); + sqlBuilder.append("UPDATE "); + sqlBuilder.append(getTableName()); + sqlBuilder.append(" SET "); + for (Map.Entry nextNewValueEntry : newValues.entrySet()) { + if (arguments.size() > 0) { + sqlBuilder.append(", "); + } + sqlBuilder.append(nextNewValueEntry.getKey()).append(" = ?"); + arguments.add(nextNewValueEntry.getValue()); + } + sqlBuilder.append(" WHERE SP_ID = ?"); + arguments.add((Number) nextRow.get("SP_ID")); + + // Apply update SQL + newJdbcTemnplate().update(sqlBuilder.toString(), arguments.toArray()); + + } + + return theRows.size(); + }); + ourLog.info("Updated {} rows on {} in {}", theRows.size(), getTableName(), sw.toString()); + }; + return myExecutor.submit(task); } public CalculateHashesTask addCalculator(String theColumnName, Function, Long> theConsumer) { @@ -116,6 +194,39 @@ public class CalculateHashesTask extends BaseTableColumnTask> myRows = new ArrayList<>(); + private List> myFutures = new ArrayList<>(); + + @Override + public void processRow(ResultSet rs) throws SQLException { + Map row = new ColumnMapRowMapper().mapRow(rs, 0); + myRows.add(row); + + if (myRows.size() >= myBatchSize) { + submitNext(); + } + } + + private void submitNext() { + if (myRows.size() > 0) { + myFutures.add(updateRows(myRows)); + myRows = new ArrayList<>(); + } + } + + public List> getFutures() { + return myFutures; + } + + public void done() { + if (myRows.size() > 0) { + submitNext(); + } + } + } + public static class MandatoryKeyMap extends ForwardingMap { diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/DropColumnTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/DropColumnTask.java index 6b6a7635bb9..2e7eb8ddf60 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/DropColumnTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/DropColumnTask.java @@ -42,7 +42,7 @@ public class DropColumnTask 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 acaf3be1c6a..45d1e68c19a 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 @@ -20,8 +20,7 @@ package ca.uhn.fhir.jpa.migrate.tasks; * #L% */ -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; import ca.uhn.fhir.jpa.migrate.taskdef.AddColumnTask; import ca.uhn.fhir.jpa.migrate.taskdef.ArbitrarySqlTask; @@ -30,13 +29,26 @@ import ca.uhn.fhir.jpa.migrate.taskdef.CalculateHashesTask; import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks; import ca.uhn.fhir.util.VersionEnum; -@SuppressWarnings({"UnstableApiUsage", "SqlNoDataSourceInspection", "SpellCheckingInspection"}) +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@SuppressWarnings({"SqlNoDataSourceInspection", "SpellCheckingInspection"}) public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { + private final Set myFlags; + /** * Constructor */ - public HapiFhirJpaMigrationTasks() { + public HapiFhirJpaMigrationTasks(Set theFlags) { + myFlags = theFlags + .stream() + .map(FlagEnum::fromCommandLineValue) + .collect(Collectors.toSet()); + init340(); init350(); init360(); @@ -60,6 +72,15 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .addColumn("OPTLOCK_VERSION") .nullable() .type(BaseTableColumnTypeTask.ColumnTypeEnum.INT); + + version.addTableRawSql("HFJ_RES_REINDEX_JOB") + .addSql(DriverTypeEnum.MSSQL_2012, "create table HFJ_RES_REINDEX_JOB (PID bigint not null, JOB_DELETED bit not null, RES_TYPE varchar(255), SUSPENDED_UNTIL datetime2, UPDATE_THRESHOLD_HIGH datetime2 not null, UPDATE_THRESHOLD_LOW datetime2, primary key (PID))") + .addSql(DriverTypeEnum.DERBY_EMBEDDED, "create table HFJ_RES_REINDEX_JOB (PID bigint not null, JOB_DELETED boolean not null, RES_TYPE varchar(255), SUSPENDED_UNTIL timestamp, UPDATE_THRESHOLD_HIGH timestamp not null, UPDATE_THRESHOLD_LOW timestamp, primary key (PID))") + .addSql(DriverTypeEnum.MARIADB_10_1, "create table HFJ_RES_REINDEX_JOB (PID bigint not null, JOB_DELETED bit not null, RES_TYPE varchar(255), SUSPENDED_UNTIL datetime(6), UPDATE_THRESHOLD_HIGH datetime(6) not null, UPDATE_THRESHOLD_LOW datetime(6), primary key (PID))") + .addSql(DriverTypeEnum.POSTGRES_9_4, "create table HFJ_RES_REINDEX_JOB (PID int8 not null, JOB_DELETED boolean not null, RES_TYPE varchar(255), SUSPENDED_UNTIL timestamp, UPDATE_THRESHOLD_HIGH timestamp not null, UPDATE_THRESHOLD_LOW timestamp, primary key (PID))") + .addSql(DriverTypeEnum.MYSQL_5_7, " create table HFJ_RES_REINDEX_JOB (PID bigint not null, JOB_DELETED bit not null, RES_TYPE varchar(255), SUSPENDED_UNTIL datetime(6), UPDATE_THRESHOLD_HIGH datetime(6) not null, UPDATE_THRESHOLD_LOW datetime(6), primary key (PID))") + .addSql(DriverTypeEnum.ORACLE_12C, "create table HFJ_RES_REINDEX_JOB (PID number(19,0) not null, JOB_DELETED number(1,0) not null, RES_TYPE varchar2(255 char), SUSPENDED_UNTIL timestamp, UPDATE_THRESHOLD_HIGH timestamp not null, UPDATE_THRESHOLD_LOW timestamp, primary key (PID))"); + } private void init350() { @@ -68,10 +89,12 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { // Forced ID changes Builder.BuilderWithTableName forcedId = version.onTable("HFJ_FORCED_ID"); version.startSectionWithMessage("Starting work on table: " + forcedId.getTableName()); + forcedId .dropIndex("IDX_FORCEDID_TYPE_FORCEDID"); forcedId .dropIndex("IDX_FORCEDID_TYPE_RESID"); + forcedId .addIndex("IDX_FORCEDID_TYPE_FID") .unique(true) @@ -80,65 +103,69 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { // Indexes - Coords Builder.BuilderWithTableName spidxCoords = version.onTable("HFJ_SPIDX_COORDS"); version.startSectionWithMessage("Starting work on table: " + spidxCoords.getTableName()); - spidxCoords - .dropIndex("IDX_SP_COORDS"); spidxCoords .addColumn("HASH_IDENTITY") .nullable() .type(AddColumnTask.ColumnTypeEnum.LONG); - spidxCoords - .addIndex("IDX_SP_COORDS_HASH") - .unique(false) - .withColumns("HASH_IDENTITY", "SP_LATITUDE", "SP_LONGITUDE"); - spidxCoords - .addTask(new CalculateHashesTask() - .setColumnName("HASH_IDENTITY") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) - ); + if (!myFlags.contains(FlagEnum.NO_MIGRATE_HASHES)) { + spidxCoords + .dropIndex("IDX_SP_COORDS"); + spidxCoords + .addIndex("IDX_SP_COORDS_HASH") + .unique(false) + .withColumns("HASH_IDENTITY", "SP_LATITUDE", "SP_LONGITUDE"); + spidxCoords + .addTask(new CalculateHashesTask() + .setColumnName("HASH_IDENTITY") + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) + ); + } // Indexes - Date Builder.BuilderWithTableName spidxDate = version.onTable("HFJ_SPIDX_DATE"); version.startSectionWithMessage("Starting work on table: " + spidxDate.getTableName()); - spidxDate - .dropIndex("IDX_SP_TOKEN"); spidxDate .addColumn("HASH_IDENTITY") .nullable() .type(AddColumnTask.ColumnTypeEnum.LONG); - spidxDate - .addIndex("IDX_SP_DATE_HASH") - .unique(false) - .withColumns("HASH_IDENTITY", "SP_VALUE_LOW", "SP_VALUE_HIGH"); - spidxDate - .addTask(new CalculateHashesTask() - .setColumnName("HASH_IDENTITY") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) - ); + if (!myFlags.contains(FlagEnum.NO_MIGRATE_HASHES)) { + spidxDate + .dropIndex("IDX_SP_TOKEN"); + spidxDate + .addIndex("IDX_SP_DATE_HASH") + .unique(false) + .withColumns("HASH_IDENTITY", "SP_VALUE_LOW", "SP_VALUE_HIGH"); + spidxDate + .addTask(new CalculateHashesTask() + .setColumnName("HASH_IDENTITY") + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) + ); + } // Indexes - Number Builder.BuilderWithTableName spidxNumber = version.onTable("HFJ_SPIDX_NUMBER"); version.startSectionWithMessage("Starting work on table: " + spidxNumber.getTableName()); - spidxNumber - .dropIndex("IDX_SP_NUMBER"); spidxNumber .addColumn("HASH_IDENTITY") .nullable() .type(AddColumnTask.ColumnTypeEnum.LONG); - spidxNumber - .addIndex("IDX_SP_NUMBER_HASH_VAL") - .unique(false) - .withColumns("HASH_IDENTITY", "SP_VALUE"); - spidxNumber - .addTask(new CalculateHashesTask() - .setColumnName("HASH_IDENTITY") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) - ); + if (!myFlags.contains(FlagEnum.NO_MIGRATE_HASHES)) { + spidxNumber + .dropIndex("IDX_SP_NUMBER"); + spidxNumber + .addIndex("IDX_SP_NUMBER_HASH_VAL") + .unique(false) + .withColumns("HASH_IDENTITY", "SP_VALUE"); + spidxNumber + .addTask(new CalculateHashesTask() + .setColumnName("HASH_IDENTITY") + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) + ); + } // Indexes - Quantity Builder.BuilderWithTableName spidxQuantity = version.onTable("HFJ_SPIDX_QUANTITY"); version.startSectionWithMessage("Starting work on table: " + spidxQuantity.getTableName()); - spidxQuantity - .dropIndex("IDX_SP_QUANTITY"); spidxQuantity .addColumn("HASH_IDENTITY") .nullable() @@ -151,61 +178,63 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .addColumn("HASH_IDENTITY_AND_UNITS") .nullable() .type(AddColumnTask.ColumnTypeEnum.LONG); - spidxQuantity - .addIndex("IDX_SP_QUANTITY_HASH") - .unique(false) - .withColumns("HASH_IDENTITY", "SP_VALUE"); - spidxQuantity - .addIndex("IDX_SP_QUANTITY_HASH_UN") - .unique(false) - .withColumns("HASH_IDENTITY_AND_UNITS", "SP_VALUE"); - spidxQuantity - .addIndex("IDX_SP_QUANTITY_HASH_SYSUN") - .unique(false) - .withColumns("HASH_IDENTITY_SYS_UNITS", "SP_VALUE"); - spidxQuantity - .addTask(new CalculateHashesTask() - .setColumnName("HASH_IDENTITY") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) - .addCalculator("HASH_IDENTITY_AND_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashUnits(t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_UNITS"))) - .addCalculator("HASH_IDENTITY_SYS_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_SYSTEM"), t.getString("SP_UNITS"))) - ); + if (!myFlags.contains(FlagEnum.NO_MIGRATE_HASHES)) { + spidxQuantity + .dropIndex("IDX_SP_QUANTITY"); + spidxQuantity + .addIndex("IDX_SP_QUANTITY_HASH") + .unique(false) + .withColumns("HASH_IDENTITY", "SP_VALUE"); + spidxQuantity + .addIndex("IDX_SP_QUANTITY_HASH_UN") + .unique(false) + .withColumns("HASH_IDENTITY_AND_UNITS", "SP_VALUE"); + spidxQuantity + .addIndex("IDX_SP_QUANTITY_HASH_SYSUN") + .unique(false) + .withColumns("HASH_IDENTITY_SYS_UNITS", "SP_VALUE"); + spidxQuantity + .addTask(new CalculateHashesTask() + .setColumnName("HASH_IDENTITY") + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) + .addCalculator("HASH_IDENTITY_AND_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashUnits(t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_UNITS"))) + .addCalculator("HASH_IDENTITY_SYS_UNITS", t -> ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_SYSTEM"), t.getString("SP_UNITS"))) + ); + } // Indexes - String Builder.BuilderWithTableName spidxString = version.onTable("HFJ_SPIDX_STRING"); version.startSectionWithMessage("Starting work on table: " + spidxString.getTableName()); - spidxString - .dropIndex("IDX_SP_STRING"); spidxString .addColumn("HASH_NORM_PREFIX") .nullable() .type(AddColumnTask.ColumnTypeEnum.LONG); - spidxString - .addIndex("IDX_SP_STRING_HASH_NRM") - .unique(false) - .withColumns("HASH_NORM_PREFIX", "SP_VALUE_NORMALIZED"); - spidxString - .addColumn("HASH_EXACT") - .nullable() - .type(AddColumnTask.ColumnTypeEnum.LONG); - spidxString - .addIndex("IDX_SP_STRING_HASH_EXCT") - .unique(false) - .withColumns("HASH_EXACT"); - spidxString - .addTask(new CalculateHashesTask() - .setColumnName("HASH_NORM_PREFIX") - .addCalculator("HASH_NORM_PREFIX", t -> ResourceIndexedSearchParamString.calculateHashNormalized(new DaoConfig(), t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_VALUE_NORMALIZED"))) - .addCalculator("HASH_EXACT", t -> ResourceIndexedSearchParamString.calculateHashExact(t.getResourceType(), t.getParamName(), t.getString("SP_VALUE_EXACT"))) - ); + if (!myFlags.contains(FlagEnum.NO_MIGRATE_HASHES)) { + spidxString + .dropIndex("IDX_SP_STRING"); + spidxString + .addIndex("IDX_SP_STRING_HASH_NRM") + .unique(false) + .withColumns("HASH_NORM_PREFIX", "SP_VALUE_NORMALIZED"); + spidxString + .addColumn("HASH_EXACT") + .nullable() + .type(AddColumnTask.ColumnTypeEnum.LONG); + spidxString + .addIndex("IDX_SP_STRING_HASH_EXCT") + .unique(false) + .withColumns("HASH_EXACT"); + spidxString + .addTask(new CalculateHashesTask() + .setColumnName("HASH_NORM_PREFIX") + .addCalculator("HASH_NORM_PREFIX", t -> ResourceIndexedSearchParamString.calculateHashNormalized(new ModelConfig(), t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_VALUE_NORMALIZED"))) + .addCalculator("HASH_EXACT", t -> ResourceIndexedSearchParamString.calculateHashExact(t.getResourceType(), t.getParamName(), t.getString("SP_VALUE_EXACT"))) + ); + } // Indexes - Token Builder.BuilderWithTableName spidxToken = version.onTable("HFJ_SPIDX_TOKEN"); version.startSectionWithMessage("Starting work on table: " + spidxToken.getTableName()); - spidxToken - .dropIndex("IDX_SP_TOKEN"); - spidxToken - .dropIndex("IDX_SP_TOKEN_UNQUAL"); spidxToken .addColumn("HASH_IDENTITY") .nullable() @@ -222,30 +251,36 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .addColumn("HASH_VALUE") .nullable() .type(AddColumnTask.ColumnTypeEnum.LONG); - spidxToken - .addIndex("IDX_SP_TOKEN_HASH") - .unique(false) - .withColumns("HASH_IDENTITY"); - spidxToken - .addIndex("IDX_SP_TOKEN_HASH_S") - .unique(false) - .withColumns("HASH_SYS"); - spidxToken - .addIndex("IDX_SP_TOKEN_HASH_SV") - .unique(false) - .withColumns("HASH_SYS_AND_VALUE"); - spidxToken - .addIndex("IDX_SP_TOKEN_HASH_V") - .unique(false) - .withColumns("HASH_VALUE"); - spidxToken - .addTask(new CalculateHashesTask() - .setColumnName("HASH_IDENTITY") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) - .addCalculator("HASH_SYS", t -> ResourceIndexedSearchParamToken.calculateHashSystem(t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"))) - .addCalculator("HASH_SYS_AND_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"), t.getString("SP_VALUE"))) - .addCalculator("HASH_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashValue(t.getResourceType(), t.getParamName(), t.getString("SP_VALUE"))) - ); + if (!myFlags.contains(FlagEnum.NO_MIGRATE_HASHES)) { + spidxToken + .dropIndex("IDX_SP_TOKEN"); + spidxToken + .dropIndex("IDX_SP_TOKEN_UNQUAL"); + spidxToken + .addIndex("IDX_SP_TOKEN_HASH") + .unique(false) + .withColumns("HASH_IDENTITY"); + spidxToken + .addIndex("IDX_SP_TOKEN_HASH_S") + .unique(false) + .withColumns("HASH_SYS"); + spidxToken + .addIndex("IDX_SP_TOKEN_HASH_SV") + .unique(false) + .withColumns("HASH_SYS_AND_VALUE"); + spidxToken + .addIndex("IDX_SP_TOKEN_HASH_V") + .unique(false) + .withColumns("HASH_VALUE"); + spidxToken + .addTask(new CalculateHashesTask() + .setColumnName("HASH_IDENTITY") + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) + .addCalculator("HASH_SYS", t -> ResourceIndexedSearchParamToken.calculateHashSystem(t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"))) + .addCalculator("HASH_SYS_AND_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"), t.getString("SP_VALUE"))) + .addCalculator("HASH_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashValue(t.getResourceType(), t.getParamName(), t.getString("SP_VALUE"))) + ); + } // Indexes - URI Builder.BuilderWithTableName spidxUri = version.onTable("HFJ_SPIDX_URI"); @@ -254,24 +289,26 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .addColumn("HASH_IDENTITY") .nullable() .type(AddColumnTask.ColumnTypeEnum.LONG); - spidxUri - .addIndex("IDX_SP_URI_HASH_IDENTITY") - .unique(false) - .withColumns("HASH_IDENTITY", "SP_URI"); - spidxUri - .addColumn("HASH_URI") - .nullable() - .type(AddColumnTask.ColumnTypeEnum.LONG); - spidxUri - .addIndex("IDX_SP_URI_HASH_URI") - .unique(false) - .withColumns("HASH_URI"); - spidxUri - .addTask(new CalculateHashesTask() - .setColumnName("HASH_IDENTITY") - .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) - .addCalculator("HASH_URI", t -> ResourceIndexedSearchParamUri.calculateHashUri(t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_URI"))) - ); + if (!myFlags.contains(FlagEnum.NO_MIGRATE_HASHES)) { + spidxUri + .addIndex("IDX_SP_URI_HASH_IDENTITY") + .unique(false) + .withColumns("HASH_IDENTITY", "SP_URI"); + spidxUri + .addColumn("HASH_URI") + .nullable() + .type(AddColumnTask.ColumnTypeEnum.LONG); + spidxUri + .addIndex("IDX_SP_URI_HASH_URI") + .unique(false) + .withColumns("HASH_URI"); + spidxUri + .addTask(new CalculateHashesTask() + .setColumnName("HASH_IDENTITY") + .addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))) + .addCalculator("HASH_URI", t -> ResourceIndexedSearchParamUri.calculateHashUri(t.getResourceType(), t.getString("SP_NAME"), t.getString("SP_URI"))) + ); + } // Search Parameter Presence Builder.BuilderWithTableName spp = version.onTable("HFJ_RES_PARAM_PRESENT"); @@ -286,7 +323,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); @@ -296,16 +333,20 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { "from HFJ_RES_PARAM_PRESENT " + "join HFJ_SEARCH_PARM ON (HFJ_SEARCH_PARM.PID = HFJ_RES_PARAM_PRESENT.SP_ID) " + "where HFJ_RES_PARAM_PRESENT.HASH_PRESENCE is null"; + consolidateSearchParamPresenceIndexesTask.addExecuteOnlyIfColumnExists("HFJ_RES_PARAM_PRESENT", "SP_ID"); consolidateSearchParamPresenceIndexesTask.addQuery(sql, ArbitrarySqlTask.QueryModeEnum.BATCH_UNTIL_NO_MORE, t -> { - Long pid = (Long) t.get("PID"); - Boolean present = (Boolean) t.get("SP_PRESENT"); + Number pid = (Number) t.get("PID"); + Boolean present = columnToBoolean(t.get("SP_PRESENT")); 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); + // SP_ID is no longer needed + spp.dropColumn("SP_ID"); + // Concept Builder.BuilderWithTableName trmConcept = version.onTable("TRM_CONCEPT"); version.startSectionWithMessage("Starting work on table: " + trmConcept.getTableName()); @@ -325,7 +366,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { // Concept Designation version.startSectionWithMessage("Starting work on table: TRM_CONCEPT_DESIG"); version - .addTable("TRM_CONCEPT_DESIG") + .addTableRawSql("TRM_CONCEPT_DESIG") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "create table TRM_CONCEPT_DESIG (PID bigint not null, LANG varchar(500), USE_CODE varchar(500), USE_DISPLAY varchar(500), USE_SYSTEM varchar(500), VAL varchar(500) not null, CS_VER_PID bigint, CONCEPT_PID bigint, primary key (PID))") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "alter table TRM_CONCEPT_DESIG add constraint FK_CONCEPTDESIG_CSV foreign key (CS_VER_PID) references TRM_CODESYSTEM_VER") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "alter table TRM_CONCEPT_DESIG add constraint FK_CONCEPTDESIG_CONCEPT foreign key (CONCEPT_PID) references TRM_CONCEPT") @@ -348,7 +389,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { // Concept Property version.startSectionWithMessage("Starting work on table: TRM_CONCEPT_PROPERTY"); version - .addTable("TRM_CONCEPT_PROPERTY") + .addTableRawSql("TRM_CONCEPT_PROPERTY") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "create table TRM_CONCEPT_PROPERTY (PID bigint not null, PROP_CODESYSTEM varchar(500), PROP_DISPLAY varchar(500), PROP_KEY varchar(500) not null, PROP_TYPE integer not null, PROP_VAL varchar(500), CS_VER_PID bigint, CONCEPT_PID bigint, primary key (PID))") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "alter table TRM_CONCEPT_PROPERTY add constraint FK_CONCEPTPROP_CSV foreign key (CS_VER_PID) references TRM_CODESYSTEM_VER") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "alter table TRM_CONCEPT_PROPERTY add constraint FK_CONCEPTPROP_CONCEPT foreign key (CONCEPT_PID) references TRM_CONCEPT") @@ -371,7 +412,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { // Concept Map - Map version.startSectionWithMessage("Starting work on table: TRM_CONCEPT_MAP"); version - .addTable("TRM_CONCEPT_MAP") + .addTableRawSql("TRM_CONCEPT_MAP") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "create table TRM_CONCEPT_MAP (PID bigint not null, RES_ID bigint, SOURCE_URL varchar(200), TARGET_URL varchar(200), URL varchar(200) not null, primary key (PID))") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "alter table TRM_CONCEPT_MAP add constraint FK_TRMCONCEPTMAP_RES foreign key (RES_ID) references HFJ_RESOURCE") .addSql(DriverTypeEnum.MYSQL_5_7, "create table TRM_CONCEPT_MAP (PID bigint not null, RES_ID bigint, SOURCE_URL varchar(200), TARGET_URL varchar(200), URL varchar(200) not null, primary key (PID))") @@ -393,7 +434,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { // Concept Map - Group version.startSectionWithMessage("Starting work on table: TRM_CONCEPT_MAP_GROUP"); version - .addTable("TRM_CONCEPT_MAP_GROUP") + .addTableRawSql("TRM_CONCEPT_MAP_GROUP") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "create table TRM_CONCEPT_MAP_GROUP (PID bigint not null, myConceptMapUrl varchar(255), SOURCE_URL varchar(200) not null, mySourceValueSet varchar(255), SOURCE_VERSION varchar(100), TARGET_URL varchar(200) not null, myTargetValueSet varchar(255), TARGET_VERSION varchar(100), CONCEPT_MAP_PID bigint not null, primary key (PID))") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "alter table TRM_CONCEPT_MAP_GROUP add constraint FK_TCMGROUP_CONCEPTMAP foreign key (CONCEPT_MAP_PID) references TRM_CONCEPT_MAP") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "create unique index IDX_CONCEPT_MAP_URL on TRM_CONCEPT_MAP (URL)") @@ -411,7 +452,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { // Concept Map - Group Element version.startSectionWithMessage("Starting work on table: TRM_CONCEPT_MAP_GRP_ELEMENT"); version - .addTable("TRM_CONCEPT_MAP_GRP_ELEMENT") + .addTableRawSql("TRM_CONCEPT_MAP_GRP_ELEMENT") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "create table TRM_CONCEPT_MAP_GRP_ELEMENT (PID bigint not null, SOURCE_CODE varchar(500) not null, myConceptMapUrl varchar(255), SOURCE_DISPLAY varchar(400), mySystem varchar(255), mySystemVersion varchar(255), myValueSet varchar(255), CONCEPT_MAP_GROUP_PID bigint not null, primary key (PID))") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "alter table TRM_CONCEPT_MAP_GRP_ELEMENT add constraint FK_TCMGELEMENT_GROUP foreign key (CONCEPT_MAP_GROUP_PID) references TRM_CONCEPT_MAP_GROUP") .addSql(DriverTypeEnum.MARIADB_10_1, "create table TRM_CONCEPT_MAP_GRP_ELEMENT (PID bigint not null, SOURCE_CODE varchar(500) not null, myConceptMapUrl varchar(255), SOURCE_DISPLAY varchar(400), mySystem varchar(255), mySystemVersion varchar(255), myValueSet varchar(255), CONCEPT_MAP_GROUP_PID bigint not null, primary key (PID))") @@ -434,7 +475,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { // Concept Map - Group Element Target version.startSectionWithMessage("Starting work on table: TRM_CONCEPT_MAP_GRP_ELM_TGT"); version - .addTable("TRM_CONCEPT_MAP_GRP_ELM_TGT") + .addTableRawSql("TRM_CONCEPT_MAP_GRP_ELM_TGT") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "create table TRM_CONCEPT_MAP_GRP_ELM_TGT (PID bigint not null, TARGET_CODE varchar(500) not null, myConceptMapUrl varchar(255), TARGET_DISPLAY varchar(400), TARGET_EQUIVALENCE varchar(50), mySystem varchar(255), mySystemVersion varchar(255), myValueSet varchar(255), CONCEPT_MAP_GRP_ELM_PID bigint not null, primary key (PID))") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "alter table TRM_CONCEPT_MAP_GRP_ELM_TGT add constraint FK_TCMGETARGET_ELEMENT foreign key (CONCEPT_MAP_GRP_ELM_PID) references TRM_CONCEPT_MAP_GRP_ELEMENT") .addSql(DriverTypeEnum.DERBY_EMBEDDED, "create index IDX_CNCPT_MP_GRP_ELM_TGT_CD on TRM_CONCEPT_MAP_GRP_ELM_TGT (TARGET_CODE)") @@ -455,6 +496,18 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .addSql(DriverTypeEnum.MSSQL_2012, "alter table TRM_CONCEPT_MAP_GRP_ELM_TGT add constraint FK_TCMGETARGET_ELEMENT foreign key (CONCEPT_MAP_GRP_ELM_PID) references TRM_CONCEPT_MAP_GRP_ELEMENT"); } + private Boolean columnToBoolean(Object theValue) { + if (theValue == null) { + return null; + } + if (theValue instanceof Boolean) { + return (Boolean) theValue; + } + + long longValue = ((Number) theValue).longValue(); + return longValue == 1L; + } + private void init340() { Builder version = forVersion(VersionEnum.V3_4_0); @@ -489,5 +542,23 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { } + public enum FlagEnum { + NO_MIGRATE_HASHES("no-migrate-350-hashes"); + + private final String myCommandLineValue; + + FlagEnum(String theCommandLineValue) { + myCommandLineValue = theCommandLineValue; + } + + public static FlagEnum fromCommandLineValue(String theCommandLineValue) { + Optional retVal = Arrays.stream(values()).filter(t -> t.myCommandLineValue.equals(theCommandLineValue)).findFirst(); + return retVal.orElseThrow(() -> { + List validValues = Arrays.stream(values()).map(t -> t.myCommandLineValue).sorted().collect(Collectors.toList()); + return new IllegalArgumentException("Invalid flag \"" + theCommandLineValue + "\". Valid values: " + validValues); + }); + } + } + } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java index 0128fb10500..7d1ef93e595 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java @@ -61,31 +61,35 @@ public class BaseMigrationTasks { } protected Builder forVersion(T theVersion) { - return new Builder(theVersion); + IAcceptsTasks sink = theTask -> { + theTask.validate(); + myTasks.put(theVersion, theTask); + }; + return new Builder(sink); } - protected class Builder { + public interface IAcceptsTasks { + void addTask(BaseTask theTask); + } - private final T myVersion; - private String myTableName; + protected static class Builder { - Builder(T theVersion) { - myVersion = theVersion; + private final IAcceptsTasks mySink; + + public Builder(IAcceptsTasks theSink) { + mySink = theSink; } public BuilderWithTableName onTable(String theTableName) { - myTableName = theTableName; - return new BuilderWithTableName(); + return new BuilderWithTableName(mySink, theTableName); } - public void addTask(BaseTask theTask) { - theTask.validate(); - myTasks.put(myVersion, theTask); + public void addTask(BaseTask theTask) { + mySink.addTask(theTask); } - public BuilderAddTable addTable(String theTableName) { - myTableName = theTableName; - return new BuilderAddTable(); + public BuilderAddTableRawSql addTableRawSql(String theTableName) { + return new BuilderAddTableRawSql(theTableName); } public Builder startSectionWithMessage(String theMessage) { @@ -94,10 +98,23 @@ public class BaseMigrationTasks { return this; } - public class BuilderWithTableName { - private String myIndexName; - private String myColumnName; - private String myForeignKeyName; + public BuilderAddTableByColumns addTableByColumns(String theTableName, String thePkColumnName) { + return new BuilderAddTableByColumns(mySink, theTableName, thePkColumnName); + } + + public void addIdGenerator(String theGeneratorName) { + AddIdGeneratorTask task = new AddIdGeneratorTask(theGeneratorName); + addTask(task); + } + + public static class BuilderWithTableName implements IAcceptsTasks { + private final String myTableName; + private final IAcceptsTasks mySink; + + public BuilderWithTableName(IAcceptsTasks theSink, String theTableName) { + mySink = theSink; + myTableName = theTableName; + } public String getTableName() { return myTableName; @@ -111,13 +128,11 @@ public class BaseMigrationTasks { } public BuilderAddIndexWithName addIndex(String theIndexName) { - myIndexName = theIndexName; - return new BuilderAddIndexWithName(); + return new BuilderAddIndexWithName(theIndexName); } public BuilderAddColumnWithName addColumn(String theColumnName) { - myColumnName = theColumnName; - return new BuilderAddColumnWithName(); + return new BuilderAddColumnWithName(theColumnName, this); } public void dropColumn(String theColumnName) { @@ -128,30 +143,38 @@ public class BaseMigrationTasks { addTask(task); } - public void addTask(BaseTableTask theTask) { - theTask.setTableName(myTableName); - Builder.this.addTask(theTask); + @Override + public void addTask(BaseTask theTask) { + ((BaseTableTask)theTask).setTableName(myTableName); + mySink.addTask(theTask); } public BuilderModifyColumnWithName modifyColumn(String theColumnName) { - myColumnName = theColumnName; - return new BuilderModifyColumnWithName(); + return new BuilderModifyColumnWithName(theColumnName); } public BuilderAddForeignKey addForeignKey(String theForeignKeyName) { - myForeignKeyName = theForeignKeyName; - return new BuilderAddForeignKey(); + return new BuilderAddForeignKey(theForeignKeyName); } public class BuilderAddIndexWithName { - private boolean myUnique; + private final String myIndexName; + + public BuilderAddIndexWithName(String theIndexName) { + myIndexName = theIndexName; + } public BuilderAddIndexUnique unique(boolean theUnique) { - myUnique = theUnique; - return new BuilderAddIndexUnique(); + return new BuilderAddIndexUnique(theUnique); } public class BuilderAddIndexUnique { + private final boolean myUnique; + + public BuilderAddIndexUnique(boolean theUnique) { + myUnique = theUnique; + } + public void withColumns(String... theColumnNames) { AddIndexTask task = new AddIndexTask(); task.setTableName(myTableName); @@ -163,15 +186,30 @@ public class BaseMigrationTasks { } } - public class BuilderAddColumnWithName { - private boolean myNullable; + public static class BuilderAddColumnWithName { + private final String myColumnName; + private final IAcceptsTasks myTaskSink; + + public BuilderAddColumnWithName(String theColumnName, IAcceptsTasks theTaskSink) { + myColumnName = theColumnName; + myTaskSink = theTaskSink; + } public BuilderAddColumnWithNameNullable nullable() { - myNullable = true; - return new BuilderAddColumnWithNameNullable(); + return new BuilderAddColumnWithNameNullable(true); + } + + public BuilderAddColumnWithNameNullable nonNullable() { + return new BuilderAddColumnWithNameNullable(false); } public class BuilderAddColumnWithNameNullable { + private final boolean myNullable; + + public BuilderAddColumnWithNameNullable(boolean theNullable) { + myNullable = theNullable; + } + public void type(AddColumnTask.ColumnTypeEnum theColumnType) { type(theColumnType, null); } @@ -184,27 +222,39 @@ public class BaseMigrationTasks { if (theLength != null) { task.setColumnLength(theLength); } - addTask(task); + myTaskSink.addTask(task); } } } public class BuilderModifyColumnWithName { - private boolean myNullable; + private final String myColumnName; + + public BuilderModifyColumnWithName(String theColumnName) { + myColumnName = theColumnName; + } + + public String getColumnName() { + return myColumnName; + } public BuilderModifyColumnWithNameAndNullable nullable() { - myNullable = true; - return new BuilderModifyColumnWithNameAndNullable(); + return new BuilderModifyColumnWithNameAndNullable(true); } public BuilderModifyColumnWithNameAndNullable nonNullable() { - myNullable = false; - return new BuilderModifyColumnWithNameAndNullable(); + return new BuilderModifyColumnWithNameAndNullable(false); } public class BuilderModifyColumnWithNameAndNullable { + private final boolean myNullable; + + public BuilderModifyColumnWithNameAndNullable(boolean theNullable) { + myNullable = theNullable; + } + public void withType(BaseTableColumnTypeTask.ColumnTypeEnum theColumnType) { withType(theColumnType, null); } @@ -235,18 +285,27 @@ public class BaseMigrationTasks { } } - public class BuilderAddForeignKey extends BuilderModifyColumnWithName { - public BuilderAddForeignKeyToColumn toColumn(String theColumnName) { - myColumnName = theColumnName; - return new BuilderAddForeignKeyToColumn(); + public class BuilderAddForeignKey { + private final String myForeignKeyName; + + public BuilderAddForeignKey(String theForeignKeyName) { + myForeignKeyName = theForeignKeyName; } - public class BuilderAddForeignKeyToColumn { + public BuilderAddForeignKeyToColumn toColumn(String theColumnName) { + return new BuilderAddForeignKeyToColumn(theColumnName); + } + + public class BuilderAddForeignKeyToColumn extends BuilderModifyColumnWithName { + public BuilderAddForeignKeyToColumn(String theColumnName) { + super(theColumnName); + } + public void references(String theForeignTable, String theForeignColumn) { AddForeignKeyTask task = new AddForeignKeyTask(); task.setTableName(myTableName); task.setConstraintName(myForeignKeyName); - task.setColumnName(myColumnName); + task.setColumnName(getColumnName()); task.setForeignTableName(theForeignTable); task.setForeignColumnName(theForeignColumn); addTask(task); @@ -255,23 +314,43 @@ public class BaseMigrationTasks { } } - public class BuilderAddTable { + public class BuilderAddTableRawSql { - private final AddTableTask myTask; + private final AddTableRawSqlTask myTask; - protected BuilderAddTable() { - myTask = new AddTableTask(); - myTask.setTableName(myTableName); + protected BuilderAddTableRawSql(String theTableName) { + myTask = new AddTableRawSqlTask(); + myTask.setTableName(theTableName); addTask(myTask); } - public BuilderAddTable addSql(DriverTypeEnum theDriverTypeEnum, @Language("SQL") String theSql) { + public BuilderAddTableRawSql addSql(DriverTypeEnum theDriverTypeEnum, @Language("SQL") String theSql) { myTask.addSql(theDriverTypeEnum, theSql); return this; } } + + public class BuilderAddTableByColumns implements IAcceptsTasks { + private final AddTableByColumnTask myTask; + + public BuilderAddTableByColumns(IAcceptsTasks theSink, String theTableName, String thePkColumnName) { + myTask = new AddTableByColumnTask(); + myTask.setTableName(theTableName); + myTask.setPkColumn(thePkColumnName); + theSink.addTask(myTask); + } + + public BuilderWithTableName.BuilderAddColumnWithName addColumn(String theColumnName) { + return new BuilderWithTableName.BuilderAddColumnWithName(theColumnName, this); + } + + @Override + public void addTask(BaseTask theTask) { + myTask.addAddColumnTask((AddColumnTask) theTask); + } + } + } - } diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIdGeneratorTaskTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIdGeneratorTaskTest.java new file mode 100644 index 00000000000..10046e76521 --- /dev/null +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIdGeneratorTaskTest.java @@ -0,0 +1,48 @@ +package ca.uhn.fhir.jpa.migrate.taskdef; + +import ca.uhn.fhir.jpa.migrate.JdbcUtils; +import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks; +import ca.uhn.fhir.util.VersionEnum; +import org.junit.Test; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertThat; + +public class AddIdGeneratorTaskTest extends BaseTest { + + + @Test + public void testAddIdGenerator() throws SQLException { + assertThat(JdbcUtils.getSequenceNames(getConnectionProperties()), empty()); + + MyMigrationTasks migrator = new MyMigrationTasks(); + getMigrator().addTasks(migrator.getTasks(VersionEnum.V3_3_0, VersionEnum.V3_6_0)); + getMigrator().migrate(); + + assertThat(JdbcUtils.getSequenceNames(getConnectionProperties()), containsInAnyOrder("SEQ_FOO")); + + // Second time, should produce no action + migrator = new MyMigrationTasks(); + getMigrator().addTasks(migrator.getTasks(VersionEnum.V3_3_0, VersionEnum.V3_6_0)); + getMigrator().migrate(); + + assertThat(JdbcUtils.getSequenceNames(getConnectionProperties()), containsInAnyOrder("SEQ_FOO")); + + } + + + + private static class MyMigrationTasks extends BaseMigrationTasks { + + public MyMigrationTasks() { + Builder v = forVersion(VersionEnum.V3_5_0); + v.addIdGenerator("SEQ_FOO"); + } + + + } + +} diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableByColumnTaskTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableByColumnTaskTest.java new file mode 100644 index 00000000000..658c1f71ab5 --- /dev/null +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableByColumnTaskTest.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.jpa.migrate.taskdef; + +import ca.uhn.fhir.jpa.migrate.JdbcUtils; +import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks; +import ca.uhn.fhir.util.VersionEnum; +import org.junit.Test; + +import java.sql.SQLException; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertThat; + +public class AddTableByColumnTaskTest extends BaseTest { + + @Test + public void testAddTable() throws SQLException { + + MyMigrationTasks migrator = new MyMigrationTasks(); + getMigrator().addTasks(migrator.getTasks(VersionEnum.V3_3_0, VersionEnum.V3_6_0)); + getMigrator().migrate(); + + assertThat(JdbcUtils.getTableNames(getConnectionProperties()), containsInAnyOrder("FOO_TABLE")); + + + } + + + private static class MyMigrationTasks extends BaseMigrationTasks { + + public MyMigrationTasks() { + Builder v = forVersion(VersionEnum.V3_5_0); + Builder.BuilderAddTableByColumns fooTable = v.addTableByColumns("FOO_TABLE", "PID"); + fooTable.addColumn("PID").nonNullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + fooTable.addColumn("HELLO").nullable().type(BaseTableColumnTypeTask.ColumnTypeEnum.STRING, 200); + } + + + } +} diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableTest.java index 22d8b2f97a2..0027d9e4d23 100644 --- a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableTest.java +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableTest.java @@ -14,7 +14,7 @@ public class AddTableTest extends BaseTest { @Test public void testTableDoesntAlreadyExist() throws SQLException { - AddTableTask task = new AddTableTask(); + AddTableRawSqlTask task = new AddTableRawSqlTask(); task.setTableName("SOMETABLE"); task.addSql(DriverTypeEnum.DERBY_EMBEDDED, "create table SOMETABLE (PID bigint not null, TEXTCOL varchar(255))"); getMigrator().addTask(task); @@ -29,7 +29,7 @@ public class AddTableTest extends BaseTest { executeSql("create table SOMETABLE (PID bigint not null, TEXTCOL varchar(255))"); assertThat(JdbcUtils.getTableNames(getConnectionProperties()), containsInAnyOrder("SOMETABLE")); - AddTableTask task = new AddTableTask(); + AddTableRawSqlTask task = new AddTableRawSqlTask(); task.setTableName("SOMETABLE"); task.addSql(DriverTypeEnum.DERBY_EMBEDDED, "create table SOMETABLE (PID bigint not null, TEXTCOL varchar(255))"); getMigrator().addTask(task); 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..9dbce75c8c7 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 @@ -1,6 +1,6 @@ package ca.uhn.fhir.jpa.migrate.taskdef; -import ca.uhn.fhir.jpa.entity.SearchParamPresent; +import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; import org.junit.Test; import java.util.List; @@ -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-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CreateHashesTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTest.java similarity index 54% rename from hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CreateHashesTest.java rename to hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTest.java index a5140a72b83..688b13a31be 100644 --- a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CreateHashesTest.java +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/CalculateHashesTest.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.jpa.migrate.taskdef; -import ca.uhn.fhir.jpa.entity.BaseResourceIndexedSearchParam; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import org.junit.Test; import org.springframework.jdbc.core.JdbcTemplate; @@ -9,7 +9,7 @@ import java.util.Map; import static org.junit.Assert.assertEquals; -public class CreateHashesTest extends BaseTest { +public class CalculateHashesTest extends BaseTest { @Test public void testCreateHashes() { @@ -50,4 +50,36 @@ public class CreateHashesTest extends BaseTest { }); } + @Test + public void testCreateHashesLargeNumber() { + executeSql("create table HFJ_SPIDX_TOKEN (SP_ID bigint not null, SP_MISSING boolean, SP_NAME varchar(100) not null, RES_ID bigint, RES_TYPE varchar(255) not null, SP_UPDATED timestamp, HASH_IDENTITY bigint, HASH_SYS bigint, HASH_SYS_AND_VALUE bigint, HASH_VALUE bigint, SP_SYSTEM varchar(200), SP_VALUE varchar(200), primary key (SP_ID))"); + + for (int i = 0; i < 777; i++) { + executeSql("insert into HFJ_SPIDX_TOKEN (SP_MISSING, SP_NAME, RES_ID, RES_TYPE, SP_UPDATED, SP_SYSTEM, SP_VALUE, SP_ID) values (false, 'identifier', 999, 'Patient', '2018-09-03 07:44:49.196', 'urn:oid:1.2.410.100110.10.41308301', '8888888" + i + "', " + i + ")"); + } + + Long count = getConnectionProperties().getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = getConnectionProperties().newJdbcTemplate(); + return jdbcTemplate.queryForObject("SELECT count(*) FROM HFJ_SPIDX_TOKEN WHERE HASH_VALUE IS NULL", Long.class); + }); + assertEquals(777L, count.longValue()); + + CalculateHashesTask task = new CalculateHashesTask(); + task.setTableName("HFJ_SPIDX_TOKEN"); + task.setColumnName("HASH_IDENTITY"); + task.addCalculator("HASH_IDENTITY", t -> BaseResourceIndexedSearchParam.calculateHashIdentity(t.getResourceType(), t.getString("SP_NAME"))); + task.addCalculator("HASH_SYS", t -> ResourceIndexedSearchParamToken.calculateHashSystem(t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"))); + task.addCalculator("HASH_SYS_AND_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(t.getResourceType(), t.getParamName(), t.getString("SP_SYSTEM"), t.getString("SP_VALUE"))); + task.addCalculator("HASH_VALUE", t -> ResourceIndexedSearchParamToken.calculateHashValue(t.getResourceType(), t.getParamName(), t.getString("SP_VALUE"))); + task.setBatchSize(3); + getMigrator().addTask(task); + + getMigrator().migrate(); + + count = getConnectionProperties().getTxTemplate().execute(t -> { + JdbcTemplate jdbcTemplate = getConnectionProperties().newJdbcTemplate(); + return jdbcTemplate.queryForObject("SELECT count(*) FROM HFJ_SPIDX_TOKEN WHERE HASH_VALUE IS NULL", Long.class); + }); + assertEquals(0L, count.longValue()); + } } diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasksTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasksTest.java index 4ff02d95a2d..4d29e9588a1 100644 --- a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasksTest.java +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasksTest.java @@ -2,11 +2,13 @@ package ca.uhn.fhir.jpa.migrate.tasks; import org.junit.Test; +import java.util.Collections; + public class HapiFhirJpaMigrationTasksTest { @Test public void testCreate() { - new HapiFhirJpaMigrationTasks(); + new HapiFhirJpaMigrationTasks(Collections.emptySet()); } diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml new file mode 100644 index 00000000000..1e63f2c8f15 --- /dev/null +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -0,0 +1,110 @@ + + 4.0.0 + + + ca.uhn.hapi.fhir + hapi-deployable-pom + 3.7.0-SNAPSHOT + ../hapi-deployable-pom/pom.xml + + + hapi-fhir-jpaserver-model + jar + + HAPI FHIR Model + + + + ca.uhn.hapi.fhir + hapi-fhir-base + ${project.version} + + + commons-logging + commons-logging + + + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu2 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu3 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-hl7org-dstu2 + ${project.version} + + + org.hibernate + hibernate-core + + + xml-apis + xml-apis + + + org.jboss.spec.javax.transaction + jboss-transaction-api_1.2_spec + + + javax.activation + activation + + + javax.activation + javax.activation-api + + + + + org.hibernate + hibernate-search-orm + + + + ch.qos.logback + logback-classic + test + + + + + + + + org.apache.maven.plugins + maven-site-plugin + + true + + + + + + + org.jacoco + jacoco-maven-plugin + + + default-prepare-agent + + prepare-agent + + + + + + + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java index 8d8d270471c..a69a16147c4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java new file mode 100644 index 00000000000..c4eb9ad7df9 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java @@ -0,0 +1,45 @@ +package ca.uhn.fhir.jpa.model.entity; + +/*- + * #%L + * HAPI FHIR Model + * %% + * 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.io.Serializable; + +public abstract class BaseResourceIndex implements Serializable { + + public abstract Long getId(); + + public abstract void setId(Long theId); + + public abstract void calculateHashes(); + + /** + * Subclasses must implement + */ + @Override + public abstract int hashCode(); + + /** + * Subclasses must implement + */ + @Override + public abstract boolean equals(Object obj); + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java similarity index 92% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java index a51d92819f4..d153691f959 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java @@ -1,17 +1,17 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * 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. @@ -31,11 +31,10 @@ import org.hibernate.search.annotations.ContainedIn; import org.hibernate.search.annotations.Field; import javax.persistence.*; -import java.io.Serializable; import java.util.Date; @MappedSuperclass -public abstract class BaseResourceIndexedSearchParam implements Serializable { +public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { static final int MAX_SP_NAME = 100; /** * Don't change this without careful consideration. You will break existing hashes! @@ -80,7 +79,8 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { // nothing } - protected abstract Long getId(); + @Override + public abstract Long getId(); public String getParamName() { return myParamName; @@ -129,8 +129,6 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { public abstract IQueryParameterType toQueryParameterType(); - public abstract void calculateHashes(); - public static long calculateHashIdentity(String theResourceType, String theParamName) { return hash(theResourceType, theParamName); } @@ -156,4 +154,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/BaseTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseTag.java similarity index 84% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseTag.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseTag.java index d1caee3bc33..9fac70c1bae 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseTag.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,10 +20,12 @@ package ca.uhn.fhir.jpa.entity; * #L% */ +import javax.persistence.Column; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; import java.io.Serializable; -import javax.persistence.*; - @MappedSuperclass public class BaseTag implements Serializable { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java index 0c451533c70..2fae75aad23 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/IBaseResourceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java similarity index 95% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/IBaseResourceEntity.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java index 0e9edeaad1b..dc9d11b0e40 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/IBaseResourceEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IBaseResourceEntity.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java new file mode 100644 index 00000000000..63534dad81d --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -0,0 +1,313 @@ +package ca.uhn.fhir.jpa.model.entity; + +/*- + * #%L + * HAPI FHIR Model + * %% + * 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.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class ModelConfig { + /** + * Default {@link #getTreatReferencesAsLogical() logical URL bases}. Includes the following + * values: + *
      + *
    • "http://hl7.org/fhir/valueset-*"
    • + *
    • "http://hl7.org/fhir/codesystem-*"
    • + *
    • "http://hl7.org/fhir/StructureDefinition/*"
    • + *
    + */ + public static final Set DEFAULT_LOGICAL_BASE_URLS = Collections.unmodifiableSet(new HashSet(Arrays.asList( + "http://hl7.org/fhir/ValueSet/*", + "http://hl7.org/fhir/CodeSystem/*", + "http://hl7.org/fhir/valueset-*", + "http://hl7.org/fhir/codesystem-*", + "http://hl7.org/fhir/StructureDefinition/*"))); + + /** + * update setter javadoc if default changes + */ + private boolean myAllowContainsSearches = false; + private boolean myAllowExternalReferences = false; + private Set myTreatBaseUrlsAsLocal = new HashSet<>(); + private Set myTreatReferencesAsLogical = new HashSet<>(DEFAULT_LOGICAL_BASE_URLS); + private boolean myDefaultSearchParamsCanBeOverridden = false; + + /** + * If set to {@code true} the default search params (i.e. the search parameters that are + * defined by the FHIR specification itself) may be overridden by uploading search + * parameters to the server with the same code as the built-in search parameter. + *

    + * This can be useful if you want to be able to disable or alter + * the behaviour of the default search parameters. + *

    + *

    + * The default value for this setting is {@code false} + *

    + */ + public boolean isDefaultSearchParamsCanBeOverridden() { + return myDefaultSearchParamsCanBeOverridden; + } + + /** + * If set to {@code true} the default search params (i.e. the search parameters that are + * defined by the FHIR specification itself) may be overridden by uploading search + * parameters to the server with the same code as the built-in search parameter. + *

    + * This can be useful if you want to be able to disable or alter + * the behaviour of the default search parameters. + *

    + *

    + * The default value for this setting is {@code false} + *

    + */ + public void setDefaultSearchParamsCanBeOverridden(boolean theDefaultSearchParamsCanBeOverridden) { + myDefaultSearchParamsCanBeOverridden = theDefaultSearchParamsCanBeOverridden; + } + + /** + * If enabled, the server will support the use of :contains searches, + * which are helpful but can have adverse effects on performance. + *

    + * Default is false (Note that prior to HAPI FHIR + * 3.5.0 the default was true) + *

    + *

    + * Note: If you change this value after data already has + * already been stored in the database, you must for a reindexing + * of all data in the database or resources may not be + * searchable. + *

    + */ + public boolean isAllowContainsSearches() { + return myAllowContainsSearches; + } + + /** + * If enabled, the server will support the use of :contains searches, + * which are helpful but can have adverse effects on performance. + *

    + * Default is false (Note that prior to HAPI FHIR + * 3.5.0 the default was true) + *

    + *

    + * Note: If you change this value after data already has + * already been stored in the database, you must for a reindexing + * of all data in the database or resources may not be + * searchable. + *

    + */ + public void setAllowContainsSearches(boolean theAllowContainsSearches) { + this.myAllowContainsSearches = theAllowContainsSearches; + } + + /** + * If set to true (default is false) the server will allow + * resources to have references to external servers. For example if this server is + * running at http://example.com/fhir and this setting is set to + * true the server will allow a Patient resource to be saved with a + * Patient.organization value of http://foo.com/Organization/1. + *

    + * Under the default behaviour if this value has not been changed, the above + * resource would be rejected by the server because it requires all references + * to be resolvable on the local server. + *

    + *

    + * Note that external references will be indexed by the server and may be searched + * (e.g. Patient:organization), but + * chained searches (e.g. Patient:organization.name) will not work across + * these references. + *

    + *

    + * It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value + * is set to true + *

    + * + * @see #setTreatBaseUrlsAsLocal(Set) + * @see #setAllowExternalReferences(boolean) + */ + public boolean isAllowExternalReferences() { + return myAllowExternalReferences; + } + + /** + * If set to true (default is false) the server will allow + * resources to have references to external servers. For example if this server is + * running at http://example.com/fhir and this setting is set to + * true the server will allow a Patient resource to be saved with a + * Patient.organization value of http://foo.com/Organization/1. + *

    + * Under the default behaviour if this value has not been changed, the above + * resource would be rejected by the server because it requires all references + * to be resolvable on the local server. + *

    + *

    + * Note that external references will be indexed by the server and may be searched + * (e.g. Patient:organization), but + * chained searches (e.g. Patient:organization.name) will not work across + * these references. + *

    + *

    + * It is recommended to also set {@link #setTreatBaseUrlsAsLocal(Set)} if this value + * is set to true + *

    + * + * @see #setTreatBaseUrlsAsLocal(Set) + * @see #setAllowExternalReferences(boolean) + */ + public void setAllowExternalReferences(boolean theAllowExternalReferences) { + myAllowExternalReferences = theAllowExternalReferences; + } + + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be replaced with + * simple local references. + *

    + * For example, if the set contains the value http://example.com/base/ + * and a resource is submitted to the server that contains a reference to + * http://example.com/base/Patient/1, the server will automatically + * convert this reference to Patient/1 + *

    + *

    + * Note that this property has different behaviour from {@link ModelConfig#getTreatReferencesAsLogical()} + *

    + * + * @see #getTreatReferencesAsLogical() + */ + public Set getTreatBaseUrlsAsLocal() { + return myTreatBaseUrlsAsLocal; + } + + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be replaced with + * simple local references. + *

    + * For example, if the set contains the value http://example.com/base/ + * and a resource is submitted to the server that contains a reference to + * http://example.com/base/Patient/1, the server will automatically + * convert this reference to Patient/1 + *

    + * + * @param theTreatBaseUrlsAsLocal The set of base URLs. May be null, which + * means no references will be treated as external + */ + public void setTreatBaseUrlsAsLocal(Set theTreatBaseUrlsAsLocal) { + if (theTreatBaseUrlsAsLocal != null) { + for (String next : theTreatBaseUrlsAsLocal) { + validateTreatBaseUrlsAsLocal(next); + } + } + + HashSet treatBaseUrlsAsLocal = new HashSet(); + for (String next : ObjectUtils.defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet())) { + while (next.endsWith("/")) { + next = next.substring(0, next.length() - 1); + } + treatBaseUrlsAsLocal.add(next); + } + myTreatBaseUrlsAsLocal = treatBaseUrlsAsLocal; + } + + /** + * Add a value to the {@link #setTreatReferencesAsLogical(Set) logical references list}. + * + * @see #setTreatReferencesAsLogical(Set) + */ + public void addTreatReferencesAsLogical(String theTreatReferencesAsLogical) { + validateTreatBaseUrlsAsLocal(theTreatReferencesAsLogical); + + if (myTreatReferencesAsLogical == null) { + myTreatReferencesAsLogical = new HashSet<>(); + } + myTreatReferencesAsLogical.add(theTreatReferencesAsLogical); + + } + + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be treated as logical + * references instead of being treated as real references. + *

    + * A logical reference is a reference which is treated as an identifier, and + * does not neccesarily resolve. See references for + * a description of logical references. For example, the valueset + * valueset-quantity-comparator is a logical + * reference. + *

    + *

    + * Values for this field may take either of the following forms: + *

    + *
      + *
    • http://example.com/some-url (will be matched exactly)
    • + *
    • http://example.com/some-base* (will match anything beginning with the part before the *)
    • + *
    + * + * @see #DEFAULT_LOGICAL_BASE_URLS Default values for this property + */ + public Set getTreatReferencesAsLogical() { + return myTreatReferencesAsLogical; + } + + /** + * This setting may be used to advise the server that any references found in + * resources that have any of the base URLs given here will be treated as logical + * references instead of being treated as real references. + *

    + * A logical reference is a reference which is treated as an identifier, and + * does not neccesarily resolve. See references for + * a description of logical references. For example, the valueset + * valueset-quantity-comparator is a logical + * reference. + *

    + *

    + * Values for this field may take either of the following forms: + *

    + *
      + *
    • http://example.com/some-url (will be matched exactly)
    • + *
    • http://example.com/some-base* (will match anything beginning with the part before the *)
    • + *
    + * + * @see #DEFAULT_LOGICAL_BASE_URLS Default values for this property + */ + public ModelConfig setTreatReferencesAsLogical(Set theTreatReferencesAsLogical) { + myTreatReferencesAsLogical = theTreatReferencesAsLogical; + return this; + } + + private static void validateTreatBaseUrlsAsLocal(String theUrl) { + Validate.notBlank(theUrl, "Base URL must not be null or empty"); + + int starIdx = theUrl.indexOf('*'); + if (starIdx != -1) { + if (starIdx != theUrl.length() - 1) { + throw new IllegalArgumentException("Base URL wildcard character (*) can only appear at the end of the string: " + theUrl); + } + } + + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceEncodingEnum.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceEncodingEnum.java similarity index 95% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceEncodingEnum.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceEncodingEnum.java index 8c650bb5cc5..ba1e424b9b6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceEncodingEnum.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceEncodingEnum.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTable.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java index 19e66707ba6..a237acce7b8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTable.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTag.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java index 90b29aee2b3..f6162d018ad 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceHistoryTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryTag.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,9 +20,8 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import java.io.Serializable; - import javax.persistence.*; +import java.io.Serializable; @Embeddable @Entity diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedCompositeStringUnique.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedCompositeStringUnique.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java index 0da8a00bc3e..cc26edf4903 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedCompositeStringUnique.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java index b7503142046..2af9bb2ec28 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamCoords.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -104,6 +104,7 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP } public Long getHashIdentity() { + calculateHashes(); return myHashIdentity; } @@ -112,10 +113,16 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP } @Override - protected Long getId() { + public Long getId() { return myId; } + @Override + public void setId(Long theId) { + myId =theId; + } + + public double getLatitude() { return myLatitude; } @@ -156,4 +163,5 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP b.append("lon", getLongitude()); return b.build(); } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java similarity index 82% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java index 7b43ebf9dce..9a81e0079c6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDate.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -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; @@ -121,6 +122,7 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar } public Long getHashIdentity() { + calculateHashes(); return myHashIdentity; } @@ -129,10 +131,15 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar } @Override - protected Long getId() { + public Long getId() { return myId; } + @Override + public void setId(Long theId) { + myId =theId; + } + protected Long getTimeFromDate(Date date) { if (date != null) { return date.getTime(); @@ -184,4 +191,32 @@ 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-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java similarity index 90% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java index ed5568fbd92..4a43d0eff09 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamNumber.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import ca.uhn.fhir.jpa.util.BigDecimalNumericFieldBridge; +import ca.uhn.fhir.jpa.model.util.BigDecimalNumericFieldBridge; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.param.NumberParam; import org.apache.commons.lang3.builder.EqualsBuilder; @@ -106,6 +106,7 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP } public Long getHashIdentity() { + calculateHashes(); return myHashIdentity; } @@ -114,10 +115,15 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP } @Override - protected Long getId() { + public Long getId() { return myId; } + @Override + public void setId(Long theId) { + myId =theId; + } + public BigDecimal getValue() { return myValue; } @@ -149,4 +155,14 @@ 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-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java similarity index 85% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java index 13c7e7fc2ad..79960e20d73 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantity.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import ca.uhn.fhir.jpa.util.BigDecimalNumericFieldBridge; +import ca.uhn.fhir.jpa.model.util.BigDecimalNumericFieldBridge; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.param.QuantityParam; import org.apache.commons.lang3.builder.EqualsBuilder; @@ -158,6 +158,7 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc } private Long getHashIdentitySystemAndUnits() { + calculateHashes(); return myHashIdentitySystemAndUnits; } @@ -166,10 +167,15 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc } @Override - protected Long getId() { + public Long getId() { return myId; } + @Override + public void setId(Long theId) { + myId =theId; + } + public String getSystem() { return mySystem; } @@ -227,6 +233,39 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc return b.build(); } + @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; + } + public static long calculateHashSystemAndUnits(String theResourceType, String theParamName, String theSystem, String theUnits) { return hash(theResourceType, theParamName, theSystem, theUnits); } @@ -235,4 +274,5 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc return hash(theResourceType, theParamName, theUnits); } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java similarity index 86% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java index 023199b395f..24f726efc47 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamString.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.util.StringNormalizer; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.param.StringParam; import org.apache.commons.lang3.StringUtils; @@ -30,8 +30,8 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.search.annotations.*; -import javax.persistence.*; import javax.persistence.Index; +import javax.persistence.*; import static org.apache.commons.lang3.StringUtils.left; @@ -145,13 +145,13 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP @Column(name = "HASH_EXACT", nullable = true) private Long myHashExact; @Transient - private transient DaoConfig myDaoConfig; + private transient ModelConfig myModelConfig; public ResourceIndexedSearchParamString() { super(); } - public ResourceIndexedSearchParamString(DaoConfig theDaoConfig, String theName, String theValueNormalized, String theValueExact) { - setDaoConfig(theDaoConfig); + public ResourceIndexedSearchParamString(ModelConfig theModelConfig, String theName, String theValueNormalized, String theValueExact) { + setModelConfig(theModelConfig); setParamName(theName); setValueNormalized(theValueNormalized); setValueExact(theValueExact); @@ -163,13 +163,14 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP @Override @PrePersist + @PreUpdate public void calculateHashes() { - if (myHashNormalizedPrefix == null && myDaoConfig != null) { + if ((myHashIdentity == null || myHashNormalizedPrefix == null || myHashExact == null) && myModelConfig != null) { String resourceType = getResourceType(); String paramName = getParamName(); String valueNormalized = getValueNormalized(); String valueExact = getValueExact(); - setHashNormalizedPrefix(calculateHashNormalized(myDaoConfig, resourceType, paramName, valueNormalized)); + setHashNormalizedPrefix(calculateHashNormalized(myModelConfig, resourceType, paramName, valueNormalized)); setHashExact(calculateHashExact(resourceType, paramName, valueExact)); setHashIdentity(calculateHashIdentity(resourceType, paramName)); } @@ -197,11 +198,17 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP b.append(getParamName(), obj.getParamName()); b.append(getResource(), obj.getResource()); b.append(getValueExact(), obj.getValueExact()); + b.append(getHashIdentity(), obj.getHashIdentity()); b.append(getHashExact(), obj.getHashExact()); b.append(getHashNormalizedPrefix(), obj.getHashNormalizedPrefix()); return b.isEquals(); } + private Long getHashIdentity() { + calculateHashes(); + return myHashIdentity; + } + public Long getHashExact() { calculateHashes(); return myHashExact; @@ -221,10 +228,16 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP } @Override - protected Long getId() { + public Long getId() { return myId; } + @Override + public void setId(Long theId) { + myId =theId; + } + + public String getValueExact() { return myValueExact; } @@ -256,8 +269,8 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP return b.toHashCode(); } - public BaseResourceIndexedSearchParam setDaoConfig(DaoConfig theDaoConfig) { - myDaoConfig = theDaoConfig; + public BaseResourceIndexedSearchParam setModelConfig(ModelConfig theModelConfig) { + myModelConfig = theModelConfig; return this; } @@ -279,7 +292,7 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP return hash(theResourceType, theParamName, theValueExact); } - public static long calculateHashNormalized(DaoConfig theDaoConfig, String theResourceType, String theParamName, String theValueNormalized) { + public static long calculateHashNormalized(ModelConfig theModelConfig, String theResourceType, String theParamName, String theValueNormalized) { /* * If we're not allowing contained searches, we'll add the first * bit of the normalized value to the hash. This helps to @@ -287,11 +300,20 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP * performance. */ int hashPrefixLength = HASH_PREFIX_LENGTH; - if (theDaoConfig.isAllowContainsSearches()) { + if (theModelConfig.isAllowContainsSearches()) { hashPrefixLength = 0; } 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 = StringNormalizer.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-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java similarity index 89% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java index 74a86253195..10d17bcf611 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamToken.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -159,6 +159,10 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa return myHashSystem; } + private void setHashSystem(Long theHashSystem) { + myHashSystem = theHashSystem; + } + private Long getHashIdentity() { calculateHashes(); return myHashIdentity; @@ -168,10 +172,6 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa myHashIdentity = theHashIdentity; } - private void setHashSystem(Long theHashSystem) { - myHashSystem = theHashSystem; - } - Long getHashSystemAndValue() { calculateHashes(); return myHashSystemAndValue; @@ -192,10 +192,15 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa } @Override - protected Long getId() { + public Long getId() { return myId; } + @Override + public void setId(Long theId) { + myId =theId; + } + public String getSystem() { return mySystem; } @@ -240,6 +245,31 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa return b.build(); } + @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; + } + public static long calculateHashSystem(String theResourceType, String theParamName, String theSystem) { return hash(theResourceType, theParamName, trim(theSystem)); } @@ -251,4 +281,5 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa public static long calculateHashValue(String theResourceType, String theParamName, String theValue) { return hash(theResourceType, theParamName, trim(theValue)); } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java similarity index 92% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java index b94ee78db6f..bf3d7e969cb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUri.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -121,6 +121,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara } private Long getHashIdentity() { + calculateHashes(); return myHashIdentity; } @@ -138,10 +139,16 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara } @Override - protected Long getId() { + public Long getId() { return myId; } + @Override + public void setId(Long theId) { + myId =theId; + } + + public String getUri() { return myUri; } @@ -175,8 +182,18 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara return b.toString(); } + @Override + public boolean matches(IQueryParameterType theParam) { + if (!(theParam instanceof UriParam)) { + return false; + } + UriParam uri = (UriParam) theParam; + return getUri().equalsIgnoreCase(uri.getValueNotNull()); + } + public static long calculateHashUri(String theResourceType, String theParamName, String theUri) { return hash(theResourceType, theParamName, theUri); } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java similarity index 95% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java index edfefe94e5a..af7c517984d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -27,7 +27,6 @@ import org.hibernate.search.annotations.Field; import org.hl7.fhir.instance.model.api.IIdType; import javax.persistence.*; -import java.io.Serializable; import java.util.Date; @Entity @@ -36,11 +35,10 @@ import java.util.Date; @Index(name = "IDX_RL_SRC", columnList = "SRC_RESOURCE_ID"), @Index(name = "IDX_RL_DEST", columnList = "TARGET_RESOURCE_ID") }) -public class ResourceLink implements Serializable { +public class ResourceLink extends BaseResourceIndex { - private static final long serialVersionUID = 1L; public static final int SRC_PATH_LENGTH = 200; - + private static final long serialVersionUID = 1L; @SequenceGenerator(name = "SEQ_RESLINK_ID", sequenceName = "SEQ_RESLINK_ID") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESLINK_ID") @Id @@ -126,10 +124,20 @@ public class ResourceLink implements Serializable { return mySourcePath; } + public void setSourcePath(String theSourcePath) { + mySourcePath = theSourcePath; + } + public ResourceTable getSourceResource() { return mySourceResource; } + public void setSourceResource(ResourceTable theSourceResource) { + mySourceResource = theSourceResource; + mySourceResourcePid = theSourceResource.getId(); + mySourceResourceType = theSourceResource.getResourceType(); + } + public Long getSourceResourcePid() { return mySourceResourcePid; } @@ -138,6 +146,13 @@ public class ResourceLink implements Serializable { return myTargetResource; } + public void setTargetResource(ResourceTable theTargetResource) { + Validate.notNull(theTargetResource); + myTargetResource = theTargetResource; + myTargetResourcePid = theTargetResource.getId(); + myTargetResourceType = theTargetResource.getResourceType(); + } + public Long getTargetResourcePid() { return myTargetResourcePid; } @@ -146,37 +161,6 @@ public class ResourceLink implements Serializable { return myTargetResourceUrl; } - public Date getUpdated() { - return myUpdated; - } - - @Override - public int hashCode() { - HashCodeBuilder b = new HashCodeBuilder(); - b.append(mySourcePath); - b.append(mySourceResource); - b.append(myTargetResourcePid); - b.append(myTargetResourceUrl); - return b.toHashCode(); - } - - public void setSourcePath(String theSourcePath) { - mySourcePath = theSourcePath; - } - - public void setSourceResource(ResourceTable theSourceResource) { - mySourceResource = theSourceResource; - mySourceResourcePid = theSourceResource.getId(); - mySourceResourceType = theSourceResource.getResourceType(); - } - - public void setTargetResource(ResourceTable theTargetResource) { - Validate.notNull(theTargetResource); - myTargetResource = theTargetResource; - myTargetResourcePid = theTargetResource.getId(); - myTargetResourceType = theTargetResource.getResourceType(); - } - public void setTargetResourceUrl(IIdType theTargetResourceUrl) { Validate.isTrue(theTargetResourceUrl.hasBaseUrl()); Validate.isTrue(theTargetResourceUrl.hasResourceType()); @@ -194,10 +178,39 @@ public class ResourceLink implements Serializable { myTargetResourceUrl = theTargetResourceUrl.getValue(); } + public Date getUpdated() { + return myUpdated; + } + public void setUpdated(Date theUpdated) { myUpdated = theUpdated; } + @Override + public Long getId() { + return myId; + } + + @Override + public void setId(Long theId) { + myId = theId; + } + + @Override + public void calculateHashes() { + // nothing right now + } + + @Override + public int hashCode() { + HashCodeBuilder b = new HashCodeBuilder(); + b.append(mySourcePath); + b.append(mySourceResource); + b.append(myTargetResourcePid); + b.append(myTargetResourceUrl); + return b.toHashCode(); + } + @Override public String toString() { StringBuilder b = new StringBuilder(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java similarity index 92% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index d8a7645459b..e59ea7add71 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import ca.uhn.fhir.jpa.search.IndexNonDeletedInterceptor; +import ca.uhn.fhir.jpa.model.search.IndexNonDeletedInterceptor; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; @@ -29,13 +29,11 @@ import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.annotations.OptimisticLock; import org.hibernate.search.annotations.*; -import javax.persistence.*; import javax.persistence.Index; +import javax.persistence.*; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.defaultString; @@ -87,10 +85,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; @@ -182,30 +176,52 @@ public class ResourceTable extends BaseHasResource implements Serializable { private Collection myParamsCompositeStringUnique; @OneToMany(mappedBy = "mySourceResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) - @IndexedEmbedded() @OptimisticLock(excluded = true) private Collection myResourceLinks; + /** + * This is a clone of {@link #myResourceLinks} but without the hibernate annotations. + * Before we persist we copy the contents of {@link #myResourceLinks} into this field. We + * have this separate because that way we can only populate this field if + * {@link #myHasLinks} is true, meaning that there are actually resource links present + * right now. This avoids Hibernate Search triggering a select on the resource link + * table. + * + * This field is used by FulltextSearchSvcImpl + * + * You can test that any changes don't cause extra queries by running + * FhirResourceDaoR4QueryCountTest + */ + @Field + @Transient + private String myResourceLinksField; + + @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()) { @@ -564,9 +580,7 @@ public class ResourceTable extends BaseHasResource implements Serializable { retVal.setPublished(getPublished()); retVal.setUpdated(getUpdated()); -// retVal.setEncoding(getEncoding()); retVal.setFhirVersion(getFhirVersion()); -// retVal.setResource(getResource()); retVal.setDeleted(getDeleted()); retVal.setForcedId(getForcedId()); @@ -590,4 +604,19 @@ public class ResourceTable extends BaseHasResource implements Serializable { return b.build(); } + @PrePersist + @PreUpdate + public void preSave() { + if (myHasLinks && myResourceLinks != null) { + myResourceLinksField = getResourceLinks() + .stream() + .map(ResourceLink::getTargetResourcePid) + .filter(Objects::nonNull) + .map(Object::toString) + .collect(Collectors.joining(" ")); + } else { + myResourceLinksField = null; + } + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java similarity index 91% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTag.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java index fd4e1320141..d74f287e983 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -19,9 +19,13 @@ package ca.uhn.fhir.jpa.entity; * limitations under the License. * #L% */ -import javax.persistence.*; -import org.apache.commons.lang3.builder.*; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.persistence.*; @Entity @Table(name = "HFJ_RES_TAG", uniqueConstraints= { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParamPresent.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParamPresent.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java index 02dbfde2433..2edd98fafb9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParamPresent.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/SearchParamPresent.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagDefinition.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagDefinition.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagDefinition.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagDefinition.java index 7dc077d795c..93db99bd75a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagDefinition.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagDefinition.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,17 +20,15 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import java.io.Serializable; -import java.util.Collection; - -import javax.persistence.*; - +import ca.uhn.fhir.model.api.Tag; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; -import ca.uhn.fhir.model.api.Tag; +import javax.persistence.*; +import java.io.Serializable; +import java.util.Collection; //@formatter:on @Entity diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagTypeEnum.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java similarity index 93% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagTypeEnum.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java index 15bce6469b3..6b9979837ef 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TagTypeEnum.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnum.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/IndexNonDeletedInterceptor.java similarity index 93% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/IndexNonDeletedInterceptor.java index e2993e5a5e7..1d9ae982970 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/IndexNonDeletedInterceptor.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/IndexNonDeletedInterceptor.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.search; +package ca.uhn.fhir.jpa.model.search; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,11 +20,10 @@ package ca.uhn.fhir.jpa.search; * #L% */ +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import org.hibernate.search.indexes.interceptor.EntityIndexingInterceptor; import org.hibernate.search.indexes.interceptor.IndexingOverride; -import ca.uhn.fhir.jpa.entity.ResourceTable; - /** * Only store non-deleted resources */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/BigDecimalNumericFieldBridge.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/BigDecimalNumericFieldBridge.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/BigDecimalNumericFieldBridge.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/BigDecimalNumericFieldBridge.java index ec5fe9f64c9..de2134a2f68 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/BigDecimalNumericFieldBridge.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/BigDecimalNumericFieldBridge.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.util; +package ca.uhn.fhir.jpa.model.util; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Model * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,13 +20,13 @@ package ca.uhn.fhir.jpa.util; * #L% */ -import java.math.BigDecimal; - import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexableField; import org.hibernate.search.bridge.LuceneOptions; import org.hibernate.search.bridge.TwoWayFieldBridge; +import java.math.BigDecimal; + public class BigDecimalNumericFieldBridge implements TwoWayFieldBridge { @Override public void set(String name, Object value, Document document, LuceneOptions luceneOptions) { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/StringNormalizer.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/StringNormalizer.java new file mode 100644 index 00000000000..9f71221e21f --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/StringNormalizer.java @@ -0,0 +1,54 @@ +package ca.uhn.fhir.jpa.model.util; + +/*- + * #%L + * HAPI FHIR Model + * %% + * 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.io.CharArrayWriter; +import java.text.Normalizer; + +public class StringNormalizer { + public static String normalizeString(String theString) { + CharArrayWriter outBuffer = new CharArrayWriter(theString.length()); + + /* + * The following block of code is used to strip out diacritical marks from latin script + * and also convert to upper case. E.g. "j?mes" becomes "JAMES". + * + * See http://www.unicode.org/charts/PDF/U0300.pdf for the logic + * behind stripping 0300-036F + * + * See #454 for an issue where we were completely stripping non latin characters + * See #832 for an issue where we normalize korean characters, which are decomposed + */ + String string = Normalizer.normalize(theString, Normalizer.Form.NFD); + for (int i = 0, n = string.length(); i < n; ++i) { + char c = string.charAt(i); + if (c >= '\u0300' && c <= '\u036F') { + continue; + } else { + outBuffer.append(c); + } + } + + return new String(outBuffer.toCharArray()).toUpperCase(); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDateTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDateTest.java similarity index 99% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDateTest.java rename to hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDateTest.java index 3ddbe995ef5..33315761d9a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDateTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamDateTest.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; import org.junit.Before; import org.junit.Test; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantityTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityTest.java similarity index 91% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantityTest.java rename to hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityTest.java index 642820ee03d..cdf17ecfbe1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantityTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamQuantityTest.java @@ -1,10 +1,10 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; import org.junit.Test; import java.math.BigDecimal; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; public class ResourceIndexedSearchParamQuantityTest { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamStringTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamStringTest.java similarity index 79% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamStringTest.java rename to hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamStringTest.java index 403cf937850..cae36b6ef52 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamStringTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamStringTest.java @@ -1,16 +1,15 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; -import ca.uhn.fhir.jpa.dao.DaoConfig; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; @SuppressWarnings("SpellCheckingInspection") public class ResourceIndexedSearchParamStringTest { @Test public void testHashFunctions() { - ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString(new DaoConfig(), "NAME", "value", "VALUE"); + ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString(new ModelConfig(), "NAME", "value", "VALUE"); token.setResource(new ResourceTable().setResourceType("Patient")); // Make sure our hashing function gives consistent results @@ -20,7 +19,7 @@ public class ResourceIndexedSearchParamStringTest { @Test public void testHashFunctionsPrefixOnly() { - ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString(new DaoConfig(), "NAME", "vZZZZZZZZZZZZZZZZ", "VZZZZZZzzzZzzzZ"); + ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString(new ModelConfig(), "NAME", "vZZZZZZZZZZZZZZZZ", "VZZZZZZzzzZzzzZ"); token.setResource(new ResourceTable().setResourceType("Patient")); // Should be the same as in testHashFunctions() diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamTokenTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamTokenTest.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamTokenTest.java rename to hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamTokenTest.java index 50f93a8617c..e6e72a00316 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamTokenTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamTokenTest.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.entity; +package ca.uhn.fhir.jpa.model.entity; import org.junit.Test; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUriTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUriTest.java similarity index 74% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUriTest.java rename to hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUriTest.java index 6b56a2287c9..9e5bf2c9dfa 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUriTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedSearchParamUriTest.java @@ -1,8 +1,10 @@ package ca.uhn.fhir.jpa.entity; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; public class ResourceIndexedSearchParamUriTest { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/TagTypeEnumTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnumTest.java similarity index 84% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/TagTypeEnumTest.java rename to hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnumTest.java index 75cf5f987fa..1036d2c69db 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/TagTypeEnumTest.java +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/entity/TagTypeEnumTest.java @@ -1,11 +1,11 @@ package ca.uhn.fhir.jpa.entity; -import static org.junit.Assert.*; - +import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; +import ca.uhn.fhir.util.TestUtil; import org.junit.AfterClass; import org.junit.Test; -import ca.uhn.fhir.util.TestUtil; +import static org.junit.Assert.assertEquals; public class TagTypeEnumTest { diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/StringNormalizerTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/StringNormalizerTest.java new file mode 100644 index 00000000000..8995d2db71f --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/util/StringNormalizerTest.java @@ -0,0 +1,14 @@ +package ca.uhn.fhir.jpa.model.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class StringNormalizerTest { + @Test + public void testNormalizeString() { + assertEquals("TEST TEST", StringNormalizer.normalizeString("TEST teSt")); + assertEquals("AEIØU", StringNormalizer.normalizeString("åéîøü")); + assertEquals("杨浩", StringNormalizer.normalizeString("杨浩")); + } +} diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml new file mode 100644 index 00000000000..72b42149b2f --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -0,0 +1,148 @@ + + 4.0.0 + + + ca.uhn.hapi.fhir + hapi-deployable-pom + 3.7.0-SNAPSHOT + ../hapi-deployable-pom/pom.xml + + + hapi-fhir-jpaserver-searchparam + jar + + HAPI FHIR Search Parameters + + + + + ca.uhn.hapi.fhir + hapi-fhir-base + ${project.version} + + + commons-logging + commons-logging + + + + + ca.uhn.hapi.fhir + hapi-fhir-client + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-server + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-model + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-validation + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu2 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu3 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-hl7org-dstu2 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-validation-resources-dstu2 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-validation-resources-dstu3 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-validation-resources-r4 + ${project.version} + + + + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + xml-apis + xml-apis + + + + + com.fasterxml.jackson.core + jackson-annotations + + + org.jscience + jscience + + + + + + ch.qos.logback + logback-classic + test + + + + + + + + + org.apache.maven.plugins + maven-site-plugin + + true + + + + + + + org.jacoco + jacoco-maven-plugin + + + default-prepare-agent + + prepare-agent + + + + + + + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/JpaRuntimeSearchParam.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/JpaRuntimeSearchParam.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/JpaRuntimeSearchParam.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/JpaRuntimeSearchParam.java index 5f9abbc6dfd..0e497f17a61 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/JpaRuntimeSearchParam.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/JpaRuntimeSearchParam.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.search; +package ca.uhn.fhir.jpa.searchparam; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java new file mode 100644 index 00000000000..a058814e888 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java @@ -0,0 +1,183 @@ +package ca.uhn.fhir.jpa.searchparam; + +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * 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.searchparam.registry.ISearchParamRegistry; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +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 ISearchParamRegistry mySearchParamRegistry; + + 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-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/ResourceMetaParams.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/ResourceMetaParams.java new file mode 100644 index 00000000000..46843b81788 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/ResourceMetaParams.java @@ -0,0 +1,63 @@ +package ca.uhn.fhir.jpa.searchparam; + +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * 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 searches + */ + public static final Map>> RESOURCE_META_AND_PARAMS; + /** + * These are parameters which are supported by searches + */ + 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-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParamConstants.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParamConstants.java new file mode 100644 index 00000000000..8441f1ae896 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParamConstants.java @@ -0,0 +1,28 @@ +package ca.uhn.fhir.jpa.searchparam; + +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * 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 SearchParamConstants { + + public static final String EXT_SP_UNIQUE = "http://hapifhir.io/fhir/StructureDefinition/sp-unique"; + + public static final String UCUM_NS = "http://unitsofmeasure.org"; +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java similarity index 83% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java index f9c38feeed7..d64a0acdeda 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.dao; +package ca.uhn.fhir.jpa.searchparam; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.IQueryParameterAnd; @@ -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; @@ -15,22 +16,24 @@ import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import java.io.Serializable; import java.util.*; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * 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. @@ -39,7 +42,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * #L% */ -public class SearchParameterMap extends LinkedHashMap>> { +public class SearchParameterMap implements Serializable { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMap.class); + + private final HashMap>> mySearchParameterMap = new LinkedHashMap<>(); private static final long serialVersionUID = 1L; @@ -105,7 +111,7 @@ public class SearchParameterMap extends LinkedHashMap theOr) { + public void add(String theName, IQueryParameterOr theOr) { if (theOr == null) { return; } @@ -116,6 +122,10 @@ public class SearchParameterMap extends LinkedHashMap>> values() { + return mySearchParameterMap.values(); + } + public SearchParameterMap add(String theName, IQueryParameterType theParam) { assert !Constants.PARAM_LASTUPDATED.equals(theName); // this has it's own field in the map @@ -261,7 +271,7 @@ public class SearchParameterMap extends LinkedHashMap> nextParamName : values()) { for (List nextAnd : nextParamName) { for (IQueryParameterType nextOr : nextAnd) { @@ -295,7 +305,7 @@ public class SearchParameterMap extends LinkedHashMap - * ?name=smith&_sort=Patient:family + * ?name=smith&_sort=Patient:family *

    *

    * This method excludes the _count parameter, @@ -336,6 +346,10 @@ public class SearchParameterMap extends LinkedHashMap> get(String theName) { + return mySearchParameterMap.get(theName); + } + + private void put(String theName, List> theParams) { + mySearchParameterMap.put(theName, theParams); + } + + public boolean containsKey(String theName) { + return mySearchParameterMap.containsKey(theName); + } + + public Set keySet() { + return mySearchParameterMap.keySet(); + } + + public boolean isEmpty() { + return mySearchParameterMap.isEmpty(); + } + + public Set>>> entrySet() { + return mySearchParameterMap.entrySet(); + } + + public List> remove(String theName) { + return mySearchParameterMap.remove(theName); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java similarity index 85% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamExtractor.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java index 1ada8b3ce86..83c1ae046a7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/BaseSearchParamExtractor.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao; +package ca.uhn.fhir.jpa.searchparam.extractor; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -23,12 +23,10 @@ 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.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.ObjectUtils; -import org.hl7.fhir.instance.model.api.IBaseDatatype; -import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; @@ -46,18 +44,18 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor @Autowired private FhirContext myContext; @Autowired - private DaoConfig myDaoConfig; - @Autowired private ISearchParamRegistry mySearchParamRegistry; + @Autowired + private ModelConfig myModelConfig; public BaseSearchParamExtractor() { super(); } - public BaseSearchParamExtractor(DaoConfig theDaoConfig, FhirContext theCtx, ISearchParamRegistry theSearchParamRegistry) { + // Used for testing + protected BaseSearchParamExtractor(FhirContext theCtx, ISearchParamRegistry theSearchParamRegistry) { myContext = theCtx; mySearchParamRegistry = theSearchParamRegistry; - myDaoConfig = theDaoConfig; } @Override @@ -82,8 +80,8 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor return myContext; } - public DaoConfig getDaoConfig() { - return myDaoConfig; + protected ModelConfig getModelConfig() { + return myModelConfig; } public Collection getSearchParams(IBaseResource theResource) { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/IResourceLinkResolver.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/IResourceLinkResolver.java new file mode 100644 index 00000000000..4179183e004 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/IResourceLinkResolver.java @@ -0,0 +1,32 @@ +package ca.uhn.fhir.jpa.searchparam.extractor; + +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * 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.RuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +public interface IResourceLinkResolver { + ResourceTable findTargetResource(RuntimeSearchParam theNextSpDef, String theNextPathsUnsplit, IIdType theNextId, String theTypeString, Class theType, String theId); + + void validateTypeOrThrowException(Class theType); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java similarity index 77% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamExtractor.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java index abe892f7b6d..ba66fd504a4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ISearchParamExtractor.java @@ -1,10 +1,15 @@ -package ca.uhn.fhir.jpa.dao; +package ca.uhn.fhir.jpa.searchparam.extractor; + +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.*; +import org.hl7.fhir.instance.model.api.IBaseResource; import java.util.List; +import java.util.Set; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -22,20 +27,6 @@ import java.util.List; * #L% */ -import java.util.Set; - -import org.hl7.fhir.instance.model.api.IBaseResource; - -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.entity.BaseResourceIndexedSearchParam; -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.ResourceIndexedSearchParamUri; -import ca.uhn.fhir.jpa.entity.ResourceTable; - public interface ISearchParamExtractor { public abstract Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/LogicalReferenceHelper.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/LogicalReferenceHelper.java new file mode 100644 index 00000000000..d752cd06c7d --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/LogicalReferenceHelper.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.jpa.searchparam.extractor; + +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * 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.model.entity.ModelConfig; +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(ModelConfig 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/PathAndRef.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRef.java similarity index 92% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/PathAndRef.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRef.java index 79615c0c120..0b8787e6fdb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/PathAndRef.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/PathAndRef.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao; +package ca.uhn.fhir.jpa.searchparam.extractor; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java new file mode 100644 index 00000000000..53a30a89ea1 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceIndexedSearchParams.java @@ -0,0 +1,370 @@ +package ca.uhn.fhir.jpa.searchparam.extractor; + +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * 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.RuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +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.compare; + +public final class ResourceIndexedSearchParams { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceIndexedSearchParams.class); + + final public Collection stringParams = new ArrayList<>(); + final public Collection tokenParams = new HashSet<>(); + final public Collection numberParams = new ArrayList<>(); + final public Collection quantityParams = new ArrayList<>(); + final public Collection dateParams = new ArrayList<>(); + final public Collection uriParams = new ArrayList<>(); + final public Collection coordsParams = new ArrayList<>(); + + final public Collection compositeStringUniques = new HashSet<>(); + final public Collection links = new HashSet<>(); + final public Set populatedResourceLinkParameters = new HashSet<>(); + + public ResourceIndexedSearchParams() { + } + + public ResourceIndexedSearchParams(ResourceTable theEntity) { + if (theEntity.isParamsStringPopulated()) { + stringParams.addAll(theEntity.getParamsString()); + } + if (theEntity.isParamsTokenPopulated()) { + tokenParams.addAll(theEntity.getParamsToken()); + } + if (theEntity.isParamsNumberPopulated()) { + numberParams.addAll(theEntity.getParamsNumber()); + } + if (theEntity.isParamsQuantityPopulated()) { + quantityParams.addAll(theEntity.getParamsQuantity()); + } + if (theEntity.isParamsDatePopulated()) { + dateParams.addAll(theEntity.getParamsDate()); + } + if (theEntity.isParamsUriPopulated()) { + uriParams.addAll(theEntity.getParamsUri()); + } + if (theEntity.isParamsCoordsPopulated()) { + coordsParams.addAll(theEntity.getParamsCoords()); + } + if (theEntity.isHasLinks()) { + links.addAll(theEntity.getResourceLinks()); + } + + if (theEntity.isParamsCompositeStringUniquePresent()) { + compositeStringUniques.addAll(theEntity.getParamsCompositeStringUnique()); + } + } + + + + public Collection getResourceLinks() { + return links; + } + + public void setParamsOn(ResourceTable theEntity) { + theEntity.setParamsString(stringParams); + theEntity.setParamsStringPopulated(stringParams.isEmpty() == false); + theEntity.setParamsToken(tokenParams); + theEntity.setParamsTokenPopulated(tokenParams.isEmpty() == false); + theEntity.setParamsNumber(numberParams); + theEntity.setParamsNumberPopulated(numberParams.isEmpty() == false); + theEntity.setParamsQuantity(quantityParams); + theEntity.setParamsQuantityPopulated(quantityParams.isEmpty() == false); + theEntity.setParamsDate(dateParams); + theEntity.setParamsDatePopulated(dateParams.isEmpty() == false); + theEntity.setParamsUri(uriParams); + theEntity.setParamsUriPopulated(uriParams.isEmpty() == false); + theEntity.setParamsCoords(coordsParams); + theEntity.setParamsCoordsPopulated(coordsParams.isEmpty() == false); + theEntity.setParamsCompositeStringUniquePresent(compositeStringUniques.isEmpty() == false); + theEntity.setResourceLinks(links); + theEntity.setHasLinks(links.isEmpty() == false); + } + + 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 + * this is needed: + *

    + * Let's say we have a unique index on (Patient:gender AND Patient:name). + * Then we pass in SMITH, John with a gender of male. + *

    + *

    + * In this case, because the name parameter matches both first and last name, + * we now need two unique indexes: + *

      + *
    • Patient?gender=male&name=SMITH
    • + *
    • Patient?gender=male&name=JOHN
    • + *
    + *

    + *

    + * So this recursive algorithm calculates those + *

    + * + * @param theResourceType E.g. Patient + * @param thePartsChoices E.g. [[gender=male], [name=SMITH, name=JOHN]] + */ + public static Set extractCompositeStringUniquesValueChains(String + theResourceType, List> thePartsChoices) { + + for (List next : thePartsChoices) { + next.removeIf(StringUtils::isBlank); + if (next.isEmpty()) { + return Collections.emptySet(); + } + } + + if (thePartsChoices.isEmpty()) { + return Collections.emptySet(); + } + + thePartsChoices.sort((o1, o2) -> { + String str1 = null; + String str2 = null; + if (o1.size() > 0) { + str1 = o1.get(0); + } + if (o2.size() > 0) { + str2 = o2.get(0); + } + return compare(str1, str2); + }); + + List values = new ArrayList<>(); + Set queryStringsToPopulate = new HashSet<>(); + extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices, values, queryStringsToPopulate); + return queryStringsToPopulate; + } + + private static void extractCompositeStringUniquesValueChains(String + theResourceType, List> thePartsChoices, List theValues, Set theQueryStringsToPopulate) { + if (thePartsChoices.size() > 0) { + List nextList = thePartsChoices.get(0); + Collections.sort(nextList); + for (String nextChoice : nextList) { + theValues.add(nextChoice); + extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices.subList(1, thePartsChoices.size()), theValues, theQueryStringsToPopulate); + theValues.remove(theValues.size() - 1); + } + } else { + if (theValues.size() > 0) { + StringBuilder uniqueString = new StringBuilder(); + uniqueString.append(theResourceType); + + for (int i = 0; i < theValues.size(); i++) { + uniqueString.append(i == 0 ? "?" : "&"); + uniqueString.append(theValues.get(i)); + } + + theQueryStringsToPopulate.add(uniqueString.toString()); + } + } + } + + + + public void calculateHashes(Collection theStringParams) { + for (BaseResourceIndex next : theStringParams) { + next.calculateHashes(); + } + } + + 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 + + '}'; + } + + public void findMissingSearchParams(ModelConfig theModelConfig, ResourceTable theEntity, Set> theActiveSearchParams) { + findMissingSearchParams(theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.STRING, stringParams); + findMissingSearchParams(theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.NUMBER, numberParams); + findMissingSearchParams(theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.QUANTITY, quantityParams); + findMissingSearchParams(theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.DATE, dateParams); + findMissingSearchParams(theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.URI, uriParams); + findMissingSearchParams(theModelConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.TOKEN, tokenParams); + } + + @SuppressWarnings("unchecked") + private void findMissingSearchParams(ModelConfig theModelConfig, 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() + .setModelConfig(theModelConfig); + break; + case TOKEN: + param = new ResourceIndexedSearchParamToken(); + break; + case URI: + param = new ResourceIndexedSearchParamUri(); + break; + case COMPOSITE: + case HAS: + case REFERENCE: + case SPECIAL: + default: + continue; + } + param.setResource(theEntity); + param.setMissing(true); + param.setParamName(nextParamName); + paramCollection.add((RT) param); + } + } + } + } + + +} diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java new file mode 100644 index 00000000000..72e35d1db1e --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java @@ -0,0 +1,223 @@ +package ca.uhn.fhir.jpa.searchparam.extractor; + +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * 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.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +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.stereotype.Service; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Service +public class ResourceLinkExtractor { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceLinkExtractor.class); + + @Autowired + private ModelConfig myModelConfig; + @Autowired + private FhirContext myContext; + @Autowired + private ISearchParamRegistry mySearchParamRegistry; + @Autowired + private ISearchParamExtractor mySearchParamExtractor; + + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + protected EntityManager myEntityManager; + + public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver) { + 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()) { + extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, theResourceLinkResolver, resourceType, nextSpDef); + } + + theEntity.setHasLinks(theParams.links.size() > 0); + } + + private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef) { + if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { + return; + } + + String nextPathsUnsplit = nextSpDef.getPath(); + if (isBlank(nextPathsUnsplit)) { + return; + } + + boolean multiType = false; + if (nextPathsUnsplit.endsWith("[x]")) { + multiType = true; + } + + List refs = mySearchParamExtractor.extractResourceLinks(theResource, nextSpDef); + for (PathAndRef nextPathAndRef : refs) { + extractResourceLinks(theParams, theEntity, theUpdateTime, theResourceLinkResolver, theResourceType, nextSpDef, nextPathsUnsplit, multiType, nextPathAndRef); + } + } + + private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, boolean theMultiType, PathAndRef nextPathAndRef) { + 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()) { + return; + } + 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 + return; + } + } else if (nextObject instanceof IBaseResource) { + nextId = ((IBaseResource) nextObject).getIdElement(); + if (nextId == null || nextId.hasIdPart() == false) { + return; + } + } else if (myContext.getElementDefinition((Class) nextObject.getClass()).getName().equals("uri")) { + return; + } else if (theResourceType.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 + return; + } else { + if (!theMultiType) { + if (nextSpDef.getName().equals("sourceuri")) { + return; + } + throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass()); + } else { + return; + } + } + + theParams.populatedResourceLinkParameters.add(nextSpDef.getName()); + + if (LogicalReferenceHelper.isLogicalReference(myModelConfig, nextId)) { + ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime); + if (theParams.links.add(resourceLink)) { + ourLog.debug("Indexing remote resource reference URL: {}", nextId); + } + return; + } + + String baseUrl = nextId.getBaseUrl(); + String typeString = nextId.getResourceType(); + if (isBlank(typeString)) { + throw new InvalidRequestException("Invalid resource reference found at path[" + theNextPathsUnsplit + "] - 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[" + theNextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue()); + } + + if (isNotBlank(baseUrl)) { + if (!myModelConfig.getTreatBaseUrlsAsLocal().contains(baseUrl) && !myModelConfig.isAllowExternalReferences()) { + String msg = myContext.getLocalizer().getMessage(BaseSearchParamExtractor.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); + } + return; + } + } + + Class type = resourceDefinition.getImplementingClass(); + String id = nextId.getIdPart(); + if (StringUtils.isBlank(id)) { + throw new InvalidRequestException("Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue()); + } + + theResourceLinkResolver.validateTypeOrThrowException(type); + ResourceLink resourceLink = createResourceLink(theEntity, theUpdateTime, theResourceLinkResolver, nextSpDef, theNextPathsUnsplit, nextPathAndRef, nextId, typeString, type, id); + if (resourceLink == null) return; + theParams.links.add(resourceLink); + } + + private ResourceLink createResourceLink(ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, PathAndRef nextPathAndRef, IIdType theNextId, String theTypeString, Class theType, String theId) { + ResourceTable targetResource = theResourceLinkResolver.findTargetResource(nextSpDef, theNextPathsUnsplit, theNextId, theTypeString, theType, theId); + + if (targetResource == null) return null; + ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, targetResource, theUpdateTime); + return resourceLink; + } + + public String toResourceName(Class theResourceType) { + return myContext.getResourceDefinition(theResourceType).getName(); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu2.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu2.java index cbcc69cec68..c305a65ec4c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu2.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao; +package ca.uhn.fhir.jpa.searchparam.extractor; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -22,7 +22,9 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.util.StringNormalizer; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.model.api.IDatatype; import ca.uhn.fhir.model.api.IPrimitiveDatatype; import ca.uhn.fhir.model.api.IValueSetEnumBinder; @@ -66,7 +68,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen searchTerm = searchTerm.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getModelConfig(), resourceName, StringNormalizer.normalizeString(searchTerm), searchTerm); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -75,7 +77,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen if (value.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { value = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getModelConfig(), nextSpDef.getName(), StringNormalizer.normalizeString(value), value); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -185,14 +187,14 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen continue; } - if (new UriDt(BaseHapiFhirDao.UCUM_NS).equals(nextValue.getSystemElement())) { + if (new UriDt(SearchParamConstants.UCUM_NS).equals(nextValue.getSystemElement())) { if (isNotBlank(nextValue.getCode())) { Unit unit = Unit.valueOf(nextValue.getCode()); javax.measure.converter.UnitConverter dayConverter = unit.getConverterTo(NonSI.DAY); double dayValue = dayConverter.convert(nextValue.getValue().doubleValue()); DurationDt newValue = new DurationDt(); - newValue.setSystem(BaseHapiFhirDao.UCUM_NS); + newValue.setSystem(SearchParamConstants.UCUM_NS); newValue.setCode(NonSI.DAY.toString()); newValue.setValue(dayValue); nextValue = newValue; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3.java index a9fb3f13995..71c042b22b5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorDstu3.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao.dstu3; +package ca.uhn.fhir.jpa.searchparam.extractor; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -23,8 +23,10 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.util.StringNormalizer; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import com.google.common.annotations.VisibleForTesting; @@ -33,8 +35,8 @@ import org.apache.commons.lang3.tuple.Pair; import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; import org.hl7.fhir.dstu3.model.*; -import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestSecurityComponent; import org.hl7.fhir.dstu3.model.Enumeration; +import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestSecurityComponent; import org.hl7.fhir.dstu3.model.Location.LocationPositionComponent; import org.hl7.fhir.dstu3.model.Patient.PatientCommunicationComponent; import org.hl7.fhir.dstu3.utils.FHIRPathEngine; @@ -68,8 +70,10 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen super(); } - public SearchParamExtractorDstu3(DaoConfig theDaoConfig, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { - super(theDaoConfig, theCtx, theSearchParamRegistry); + // This constructor is used by tests + @VisibleForTesting + public SearchParamExtractorDstu3(ModelConfig theModelConfig, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { + super(theCtx, theSearchParamRegistry); myValidationSupport = theValidationSupport; } @@ -92,7 +96,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen searchTerm = searchTerm.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getModelConfig(), resourceName, StringNormalizer.normalizeString(searchTerm), searchTerm); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -101,7 +105,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen if (value.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { value = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getModelConfig(), nextSpDef.getName(), StringNormalizer.normalizeString(value), value); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -253,14 +257,14 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen continue; } - if (BaseHapiFhirDao.UCUM_NS.equals(nextValue.getSystem())) { + if (SearchParamConstants.UCUM_NS.equals(nextValue.getSystem())) { if (isNotBlank(nextValue.getCode())) { Unit unit = Unit.valueOf(nextValue.getCode()); javax.measure.converter.UnitConverter dayConverter = unit.getConverterTo(NonSI.DAY); double dayValue = dayConverter.convert(nextValue.getValue().doubleValue()); Duration newValue = new Duration(); - newValue.setSystem(BaseHapiFhirDao.UCUM_NS); + newValue.setSystem(SearchParamConstants.UCUM_NS); newValue.setCode(NonSI.DAY.toString()); newValue.setValue(dayValue); nextValue = newValue; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java index e5c2d491ab2..4dc42f54007 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorR4.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao.r4; +package ca.uhn.fhir.jpa.searchparam.extractor; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -23,8 +23,10 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.dao.*; -import ca.uhn.fhir.jpa.entity.*; +import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.util.StringNormalizer; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import com.google.common.annotations.VisibleForTesting; @@ -38,8 +40,8 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestSecurityComponent; import org.hl7.fhir.r4.model.Enumeration; +import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestSecurityComponent; import org.hl7.fhir.r4.model.Location.LocationPositionComponent; import org.hl7.fhir.r4.model.Patient.PatientCommunicationComponent; import org.hl7.fhir.r4.utils.FHIRPathEngine; @@ -49,12 +51,9 @@ import javax.measure.unit.NonSI; import javax.measure.unit.Unit; import java.math.BigDecimal; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; -import static org.apache.commons.lang3.StringUtils.trim; public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements ISearchParamExtractor { @@ -69,8 +68,10 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements super(); } - public SearchParamExtractorR4(DaoConfig theDaoConfig, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { - super(theDaoConfig, theCtx, theSearchParamRegistry); + // This constructor is used by tests + @VisibleForTesting + public SearchParamExtractorR4(ModelConfig theModelConfig, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { + super(theCtx, theSearchParamRegistry); myValidationSupport = theValidationSupport; } @@ -93,7 +94,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements searchTerm = searchTerm.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getModelConfig(), resourceName, StringNormalizer.normalizeString(searchTerm), searchTerm); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -102,7 +103,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements if (value.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { value = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getModelConfig(), nextSpDef.getName(), StringNormalizer.normalizeString(value), value); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -251,14 +252,14 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements continue; } - if (BaseHapiFhirDao.UCUM_NS.equals(nextValue.getSystem())) { + if (SearchParamConstants.UCUM_NS.equals(nextValue.getSystem())) { if (isNotBlank(nextValue.getCode())) { Unit unit = Unit.valueOf(nextValue.getCode()); javax.measure.converter.UnitConverter dayConverter = unit.getConverterTo(NonSI.DAY); double dayValue = dayConverter.convert(nextValue.getValue().doubleValue()); Duration newValue = new Duration(); - newValue.setSystem(BaseHapiFhirDao.UCUM_NS); + newValue.setSystem(SearchParamConstants.UCUM_NS); newValue.setCode(NonSI.DAY.toString()); newValue.setValue(dayValue); nextValue = newValue; diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java new file mode 100644 index 00000000000..d8deeed6821 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java @@ -0,0 +1,88 @@ +package ca.uhn.fhir.jpa.searchparam.extractor; + +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * 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.model.entity.*; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import java.util.Set; + +@Service +@Lazy +public class SearchParamExtractorService { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamExtractorService.class); + + @Autowired + private ISearchParamExtractor mySearchParamExtractor; + + 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); + } + } + } + + 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); + } + + +} + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java similarity index 87% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamRegistry.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java index 12d31134cc2..737b23919c6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamRegistry.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao; +package ca.uhn.fhir.jpa.searchparam.registry; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -23,29 +23,28 @@ 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.search.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.util.SearchParameterUtil; import ca.uhn.fhir.util.StopWatch; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.instance.model.api.IBaseResource; 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.transaction.PlatformTransactionManager; -import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.PostConstruct; import java.util.*; import static org.apache.commons.lang3.StringUtils.isBlank; -public abstract class BaseSearchParamRegistry implements ISearchParamRegistry, ApplicationContextAware { +public abstract class BaseSearchParamRegistry implements ISearchParamRegistry { private static final int MAX_MANAGED_PARAM_COUNT = 10000; private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamRegistry.class); @@ -54,16 +53,21 @@ public abstract class BaseSearchParamRegistry implemen private volatile Map, List>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap(); @Autowired private FhirContext myCtx; - private Collection> myResourceDaos; private volatile Map> myActiveSearchParams; @Autowired - private DaoConfig myDaoConfig; + private ModelConfig myModelConfig; private volatile long myLastRefresh; private ApplicationContext myApplicationContext; - @Autowired - private PlatformTransactionManager myTxManager; - public BaseSearchParamRegistry() { + private ISearchParamProvider mySearchParamProvider; + + public BaseSearchParamRegistry(ISearchParamProvider theSearchParamProvider) { super(); + mySearchParamProvider = theSearchParamProvider; + } + + @VisibleForTesting + public void setSearchParamProvider(ISearchParamProvider theSearchParamProvider) { + mySearchParamProvider = theSearchParamProvider; } @Override @@ -137,8 +141,6 @@ public abstract class BaseSearchParamRegistry implemen return retVal; } - public abstract IFhirResourceDao getSearchParameterDao(); - private void populateActiveSearchParams(Map> theActiveSearchParams) { Map> activeUniqueSearchParams = new HashMap<>(); Map, List>> activeParamNamesToUniqueSearchParams = new HashMap<>(); @@ -218,14 +220,10 @@ public abstract class BaseSearchParamRegistry implemen public void postConstruct() { Map> resourceNameToSearchParams = new HashMap<>(); - myResourceDaos = new ArrayList<>(); - Map daos = myApplicationContext.getBeansOfType(IFhirResourceDao.class, false, false); - for (IFhirResourceDao next : daos.values()) { - myResourceDaos.add(next); - } + Set resourceNames = myCtx.getResourceNames(); - for (IFhirResourceDao nextDao : myResourceDaos) { - RuntimeResourceDefinition nextResDef = myCtx.getResourceDefinition(nextDao.getResourceType()); + for (String resourceName : resourceNames) { + RuntimeResourceDefinition nextResDef = myCtx.getResourceDefinition(resourceName); String nextResourceName = nextResDef.getName(); HashMap nameToParam = new HashMap<>(); resourceNameToSearchParams.put(nextResourceName, nameToParam); @@ -245,16 +243,12 @@ public abstract class BaseSearchParamRegistry implemen long refreshInterval = 60 * DateUtils.MILLIS_PER_MINUTE; if (System.currentTimeMillis() - refreshInterval > myLastRefresh) { synchronized (this) { - TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); - txTemplate.execute(t->{ - doRefresh(refreshInterval); - return null; - }); + mySearchParamProvider.refreshCache(this, refreshInterval); } } } - private void doRefresh(long theRefreshInterval) { + public void doRefresh(long theRefreshInterval) { if (System.currentTimeMillis() - theRefreshInterval > myLastRefresh) { StopWatch sw = new StopWatch(); @@ -269,7 +263,7 @@ public abstract class BaseSearchParamRegistry implemen SearchParameterMap params = new SearchParameterMap(); params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT); - IBundleProvider allSearchParamsBp = getSearchParameterDao().search(params); + IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params); int size = allSearchParamsBp.size(); // Just in case.. @@ -297,7 +291,7 @@ public abstract class BaseSearchParamRegistry implemen Map searchParamMap = getSearchParamMap(searchParams, nextBaseName); String name = runtimeSp.getName(); - if (myDaoConfig.isDefaultSearchParamsCanBeOverridden() || !searchParamMap.containsKey(name)) { + if (myModelConfig.isDefaultSearchParamsCanBeOverridden() || !searchParamMap.containsKey(name)) { searchParamMap.put(name, runtimeSp); } @@ -341,12 +335,19 @@ public abstract class BaseSearchParamRegistry implemen refreshCacheIfNecessary(); } - @Override - public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException { - myApplicationContext = theApplicationContext; - } 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-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamProvider.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamProvider.java new file mode 100644 index 00000000000..6ee4122ec78 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamProvider.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.jpa.searchparam.registry; + +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * 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.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public interface ISearchParamProvider { + IBundleProvider search(SearchParameterMap theParams); + + void refreshCache(BaseSearchParamRegistry theSPBaseSearchParamRegistry, long theRefreshInterval); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java similarity index 78% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java index 72b89d670b7..9d44ed27be5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/ISearchParamRegistry.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao; +package ca.uhn.fhir.jpa.searchparam.registry; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -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 ca.uhn.fhir.jpa.searchparam.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/SearchParamRegistryDstu2.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu2.java similarity index 88% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamRegistryDstu2.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu2.java index 2c2e8babd7a..20d7bf3d6a4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamRegistryDstu2.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu2.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao; +package ca.uhn.fhir.jpa.searchparam.registry; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -21,28 +21,26 @@ package ca.uhn.fhir.jpa.dao; */ import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; -import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.dstu2.resource.SearchParameter; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.util.DatatypeUtil; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.springframework.beans.factory.annotation.Autowired; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; import static org.apache.commons.lang3.StringUtils.isBlank; public class SearchParamRegistryDstu2 extends BaseSearchParamRegistry { - @Autowired - private IFhirResourceDao mySpDao; - - @Override - public IFhirResourceDao getSearchParameterDao() { - return mySpDao; +public SearchParamRegistryDstu2(ISearchParamProvider theSearchParamProvider) { + super(theSearchParamProvider); } @Override @@ -104,7 +102,7 @@ public class SearchParamRegistryDstu2 extends BaseSearchParamRegistry uniqueExts = theNextSp.getUndeclaredExtensionsByUrl(JpaConstants.EXT_SP_UNIQUE); + List uniqueExts = theNextSp.getUndeclaredExtensionsByUrl(SearchParamConstants.EXT_SP_UNIQUE); if (uniqueExts.size() > 0) { IPrimitiveType uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive(); if (uniqueExtsValuePrimitive != null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamRegistryDstu3.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu3.java similarity index 87% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamRegistryDstu3.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu3.java index 7dc2eb5aff2..cb4e6c065aa 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamRegistryDstu3.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu3.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao.dstu3; +package ca.uhn.fhir.jpa.searchparam.registry; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -21,17 +21,14 @@ package ca.uhn.fhir.jpa.dao.dstu3; */ import ca.uhn.fhir.context.RuntimeSearchParam.RuntimeSearchParamStatusEnum; -import ca.uhn.fhir.jpa.dao.BaseSearchParamRegistry; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; -import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.util.DatatypeUtil; import org.hl7.fhir.dstu3.model.Extension; import org.hl7.fhir.dstu3.model.SearchParameter; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.Collections; @@ -42,12 +39,8 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry { - @Autowired - private IFhirResourceDao mySpDao; - - @Override - public IFhirResourceDao getSearchParameterDao() { - return mySpDao; + public SearchParamRegistryDstu3(ISearchParamProvider theSearchParamProvider) { + super(theSearchParamProvider); } @Override @@ -116,7 +109,7 @@ public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry uniqueExts = theNextSp.getExtensionsByUrl(JpaConstants.EXT_SP_UNIQUE); + List uniqueExts = theNextSp.getExtensionsByUrl(SearchParamConstants.EXT_SP_UNIQUE); if (uniqueExts.size() > 0) { IPrimitiveType uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive(); if (uniqueExtsValuePrimitive != null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamRegistryR4.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryR4.java similarity index 87% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamRegistryR4.java rename to hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryR4.java index 2deb0089087..6ecdd680093 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamRegistryR4.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryR4.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.dao.r4; +package ca.uhn.fhir.jpa.searchparam.registry; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Search Parameters * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -22,10 +22,8 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam.RuntimeSearchParamStatusEnum; -import ca.uhn.fhir.jpa.dao.BaseSearchParamRegistry; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; -import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.util.DatatypeUtil; import org.hl7.fhir.instance.model.api.IIdType; @@ -33,7 +31,6 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.SearchParameter; -import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.Collections; @@ -44,12 +41,8 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class SearchParamRegistryR4 extends BaseSearchParamRegistry { - @Autowired - private IFhirResourceDao mySpDao; - - @Override - public IFhirResourceDao getSearchParameterDao() { - return mySpDao; + public SearchParamRegistryR4(ISearchParamProvider theSearchParamProvider) { + super(theSearchParamProvider); } @Override @@ -118,7 +111,7 @@ public class SearchParamRegistryR4 extends BaseSearchParamRegistry uniqueExts = theNextSp.getExtensionsByUrl(JpaConstants.EXT_SP_UNIQUE); + List uniqueExts = theNextSp.getExtensionsByUrl(SearchParamConstants.EXT_SP_UNIQUE); if (uniqueExts.size() > 0) { IPrimitiveType uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive(); if (uniqueExtsValuePrimitive != null) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/IndexStressTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/IndexStressTest.java similarity index 83% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/IndexStressTest.java rename to hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/IndexStressTest.java index 0b963c30101..ebb536ab08a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/IndexStressTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/IndexStressTest.java @@ -1,12 +1,12 @@ -package ca.uhn.fhir.jpa.stresstest; +package ca.uhn.fhir.jpa.searchparam; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; -import ca.uhn.fhir.jpa.dao.dstu3.SearchParamExtractorDstu3; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.util.StopWatch; import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.CachingValidationSupport; @@ -21,7 +21,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -36,12 +36,11 @@ public class IndexStressTest { p.getMaritalStatus().setText("DDDDD"); p.addAddress().addLine("A").addLine("B").addLine("C"); - DaoConfig daoConfig = new DaoConfig(); FhirContext ctx = FhirContext.forDstu3(); IValidationSupport mockValidationSupport = mock(IValidationSupport.class); IValidationSupport validationSupport = new CachingValidationSupport(new ValidationSupportChain(new DefaultProfileValidationSupport(), mockValidationSupport)); ISearchParamRegistry searchParamRegistry = mock(ISearchParamRegistry.class); - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(daoConfig, ctx, validationSupport, searchParamRegistry); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ctx, validationSupport, searchParamRegistry); extractor.start(); Map spMap = ctx diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParamExtractorDstu3Test.java similarity index 77% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java rename to hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParamExtractorDstu3Test.java index 068acbd9de4..d246db95632 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParamExtractorDstu3Test.java @@ -1,29 +1,25 @@ -package ca.uhn.fhir.jpa.dao.dstu3; +package ca.uhn.fhir.jpa.searchparam; -import static org.junit.Assert.assertEquals; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; -import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; +import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; import org.hl7.fhir.dstu3.model.Observation; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -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.entity.BaseResourceIndexedSearchParam; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; -import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.util.TestUtil; +import java.util.*; + +import static org.junit.Assert.assertEquals; public class SearchParamExtractorDstu3Test { @@ -80,9 +76,19 @@ 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); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); extractor.start(); Set tokens = extractor.extractSearchParamTokens(new ResourceTable(), obs); assertEquals(1, tokens.size()); diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml new file mode 100644 index 00000000000..5dfb07771a6 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -0,0 +1,77 @@ + + 4.0.0 + + + ca.uhn.hapi.fhir + hapi-deployable-pom + 3.7.0-SNAPSHOT + ../hapi-deployable-pom/pom.xml + + + hapi-fhir-jpaserver-subscription + jar + + HAPI FHIR Subscription Server + + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-searchparam + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-model + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-validation + ${project.version} + + + + + org.springframework + spring-test + test + + + ch.qos.logback + logback-classic + test + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-site-plugin + + true + + + + + + + org.jacoco + jacoco-maven-plugin + + + default-prepare-agent + + prepare-agent + + + + + + + diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProvider.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProvider.java new file mode 100644 index 00000000000..50b1b5de76c --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProvider.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.jpa.subscription; + +/*- + * #%L + * HAPI FHIR Subscription 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.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.registry.BaseSearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; +import ca.uhn.fhir.rest.api.CacheControlDirective; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.server.SimpleBundleProvider; +import ca.uhn.fhir.util.BundleUtil; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class FhirClientSearchParamProvider implements ISearchParamProvider { + + private final IGenericClient myClient; + + public FhirClientSearchParamProvider(IGenericClient theClient) { + myClient = theClient; + } + + @Override + public IBundleProvider search(SearchParameterMap theParams) { + FhirContext fhirContext = myClient.getFhirContext(); + + IBaseBundle bundle = myClient + .search() + .forResource(ResourceTypeEnum.SEARCHPARAMETER.getCode()) + .cacheControl(new CacheControlDirective().setNoCache(true)) + .execute(); + + return new SimpleBundleProvider(BundleUtil.toListOfResources(fhirContext, bundle)); + } + + @Override + public void refreshCache(BaseSearchParamRegistry theSearchParamRegistry, long theRefreshInterval) { + theSearchParamRegistry.doRefresh(theRefreshInterval); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java similarity index 73% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java index caaccfbcaee..c02bae8cf16 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.subscription; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -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-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionConfig.java new file mode 100644 index 00000000000..18548668e27 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionConfig.java @@ -0,0 +1,44 @@ +package ca.uhn.fhir.jpa.subscription.config; + +/*- + * #%L + * HAPI FHIR Subscription 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.jpa.searchparam.registry.ISearchParamProvider; +import ca.uhn.fhir.jpa.subscription.FhirClientSearchParamProvider; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan(basePackages = "ca.uhn.fhir.jpa") +public abstract class BaseSubscriptionConfig { + @Autowired + IGenericClient myClient; + + public abstract FhirContext fhirContext(); + + @Bean + protected ISearchParamProvider searchParamProvider() { + return new FhirClientSearchParamProvider(myClient); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionDstu3Config.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionDstu3Config.java new file mode 100644 index 00000000000..939b92b0893 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionDstu3Config.java @@ -0,0 +1,68 @@ +package ca.uhn.fhir.jpa.subscription.config; + +/*- + * #%L + * HAPI FHIR Subscription 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.ParserOptions; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu3; +import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; +import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +// From BaseDstu3Config +public class BaseSubscriptionDstu3Config extends BaseSubscriptionConfig { + @Override + public FhirContext fhirContext() { + return fhirContextDstu3(); + } + + @Bean + @Primary + public FhirContext fhirContextDstu3() { + FhirContext retVal = FhirContext.forDstu3(); + + // Don't strip versions in some places + ParserOptions parserOptions = retVal.getParserOptions(); + parserOptions.setDontStripVersionsFromReferencesAtPaths("AuditEvent.entity.reference"); + + return retVal; + } + + @Bean + public ISearchParamRegistry searchParamRegistry() { + return new SearchParamRegistryDstu3(searchParamProvider()); + } + + @Bean(autowire = Autowire.BY_TYPE) + public SearchParamExtractorDstu3 searchParamExtractor() { + return new SearchParamExtractorDstu3(); + } + + @Primary + @Bean(autowire = Autowire.BY_NAME, name = "myJpaValidationSupportChainDstu3") + public IValidationSupport validationSupportChainDstu3() { + return new DefaultProfileValidationSupport(); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/CriteriaResourceMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/CriteriaResourceMatcher.java new file mode 100644 index 00000000000..aa9d1fc5eaf --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/CriteriaResourceMatcher.java @@ -0,0 +1,159 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +/*- + * #%L + * HAPI FHIR Subscription 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.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.searchparam.MatchUrlService; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +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 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-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java new file mode 100644 index 00000000000..9d645a5b719 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +/*- + * #%L + * HAPI FHIR Subscription 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.subscription.ResourceModifiedMessage; + +public interface ISubscriptionMatcher { + SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg); +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/InlineResourceLinkResolver.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/InlineResourceLinkResolver.java new file mode 100644 index 00000000000..992ab36a074 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/InlineResourceLinkResolver.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +/*- + * #%L + * HAPI FHIR Subscription 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.RuntimeSearchParam; +import ca.uhn.fhir.jpa.model.entity.ForcedId; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.springframework.stereotype.Service; + +@Service +public class InlineResourceLinkResolver implements IResourceLinkResolver { + + @Override + public ResourceTable findTargetResource(RuntimeSearchParam theNextSpDef, String theNextPathsUnsplit, IIdType theNextId, String theTypeString, Class theType, String theId) { + ResourceTable target; + target = new ResourceTable(); + target.setResourceType(theTypeString); + if (theNextId.isIdPartValidLong()) { + target.setId(theNextId.getIdPartAsLong()); + } else { + ForcedId forcedId = new ForcedId(); + forcedId.setForcedId(theId); + target.setForcedId(forcedId); + } + return target; + } + + @Override + public void validateTypeOrThrowException(Class theType) { + // When resolving reference in-memory for a single resource, there's nothing to validate + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatchResult.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatchResult.java new file mode 100644 index 00000000000..620116fc4d0 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/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 Subscription 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-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java new file mode 100644 index 00000000000..bb9005c6958 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java @@ -0,0 +1,70 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +/*- + * #%L + * HAPI FHIR Subscription 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.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; +import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor; +import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; +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.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; + @Autowired + private ResourceLinkExtractor myResourceLinkExtractor; + @Autowired + private InlineResourceLinkResolver myInlineResourceLinkResolver; + + @Override + 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); + myResourceLinkExtractor.extractResourceLinks(searchParams, entity, resource, resource.getMeta().getLastUpdated(), myInlineResourceLinkResolver); + RuntimeResourceDefinition resourceDefinition = myContext.getResourceDefinition(resource); + return myCriteriaResourceMatcher.match(criteria, resourceDefinition, searchParams); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDstu3Test.java new file mode 100644 index 00000000000..b06f3434592 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDstu3Test.java @@ -0,0 +1,8 @@ +package ca.uhn.fhir.jpa.subscription; + +import ca.uhn.fhir.jpa.subscription.config.TestSubscriptionDstu3Config; +import org.springframework.test.context.ContextConfiguration; + +@ContextConfiguration(classes = {TestSubscriptionDstu3Config.class}) +public abstract class BaseSubscriptionDstu3Test extends BaseSubscriptionTest { +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionTest.java new file mode 100644 index 00000000000..c62a44c766e --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionTest.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.jpa.subscription; + +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.subscription.config.MockSearchParamProvider; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +public abstract class BaseSubscriptionTest { + + @Autowired + ISearchParamProvider mySearchParamProvider; + + @Autowired + ISearchParamRegistry mySearchParamRegistry; + + public void setSearchParamBundleResponse(IBundleProvider theBundleProvider) { + ((MockSearchParamProvider)mySearchParamProvider).setBundleProvider(theBundleProvider); + mySearchParamRegistry.forceRefresh(); + } + + +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/MockSearchParamProvider.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/MockSearchParamProvider.java new file mode 100644 index 00000000000..071e3c24bce --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/MockSearchParamProvider.java @@ -0,0 +1,23 @@ +package ca.uhn.fhir.jpa.subscription.config; + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.subscription.FhirClientSearchParamProvider; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.server.SimpleBundleProvider; + +public class MockSearchParamProvider extends FhirClientSearchParamProvider { + private IBundleProvider myBundleProvider = new SimpleBundleProvider(); + + public MockSearchParamProvider() { + super(null); + } + + public void setBundleProvider(IBundleProvider theBundleProvider) { + myBundleProvider = theBundleProvider; + } + + @Override + public IBundleProvider search(SearchParameterMap theParams) { + return myBundleProvider; + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionConfig.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionConfig.java new file mode 100644 index 00000000000..a974094f491 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionConfig.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.jpa.subscription.config; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.util.PortUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TestSubscriptionConfig { + + @Autowired + FhirContext myFhirContext; + private static int ourPort; + private static String ourServerBase; + + @Bean + public ModelConfig modelConfig() { + return new ModelConfig(); + } + + @Bean + public IGenericClient fhirClient() { + ourPort = PortUtil.findFreePort(); + ourServerBase = "http://localhost:" + ourPort + "/fhir/context"; + + return myFhirContext.newRestfulGenericClient(ourServerBase); + }; +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionDstu3Config.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionDstu3Config.java new file mode 100644 index 00000000000..be4dc5d128b --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionDstu3Config.java @@ -0,0 +1,25 @@ +package ca.uhn.fhir.jpa.subscription.config; + +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu3; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(TestSubscriptionConfig.class) +public class TestSubscriptionDstu3Config extends BaseSubscriptionDstu3Config { + @Bean + @Override + public ISearchParamProvider searchParamProvider() { + return new MockSearchParamProvider(); + } + + @Bean + @Override + public ISearchParamRegistry searchParamRegistry() { + return new SearchParamRegistryDstu3(searchParamProvider()); + } + +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR3.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR3.java new file mode 100644 index 00000000000..ca60086a3ab --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR3.java @@ -0,0 +1,493 @@ +package ca.uhn.fhir.jpa.subscription.matcher; + +import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDstu3Test; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.server.SimpleBundleProvider; +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 java.util.Arrays; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SubscriptionMatcherInMemoryTestR3 extends BaseSubscriptionDstu3Test { + @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(SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + + IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); + setSearchParamBundleResponse(bundle); + + { + 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(SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + + IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); + setSearchParamBundleResponse(bundle); + + { + 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(SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + + IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); + setSearchParamBundleResponse(bundle); + + { + 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-uhnfhirtest/derby_maintenance.txt b/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt index 8899c04f40f..223f6981025 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt +++ b/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt @@ -121,6 +121,9 @@ drop table hfj_search_result cascade constraints; drop table hfj_search_include cascade constraints; drop table hfj_search cascade constraints; drop table hfj_res_param_present cascade constraints; +DROP TABLE HFJ_RES_REINDEX_JOB cascade constraints; +DROP TABLE HFJ_SEARCH_PARM cascade constraints; +DROP TABLE HFJ_TAG_DEF cascade CONSTRAINTS; drop table hfj_idx_cmp_string_uniq cascade constraints; drop table hfj_subscription_stats cascade constraints; drop table trm_concept_property cascade constraints; @@ -130,11 +133,11 @@ drop table trm_codesystem_ver cascade constraints; drop table trm_codesystem cascade constraints; DROP TABLE hfj_resource CASCADE CONSTRAINTS; DROP TABLE hfj_res_ver CASCADE CONSTRAINTS; -drop table cdr_audit_evt_target_module cascade constraints; -drop table cdr_audit_evt_target_res cascade constraints; -drop table cdr_audit_evt_target_user cascade constraints; -drop table cdr_xact_log_step cascade constraints; -drop table cdr_xact_log cascade constraints; +DROP TABLE TRM_CONCEPT_DESIG CASCADE CONSTRAINTS; +DROP TABLE TRM_CONCEPT_MAP CASCADE CONSTRAINTS; +DROP TABLE TRM_CONCEPT_MAP_GROUP CASCADE CONSTRAINTS; +DROP TABLE TRM_CONCEPT_MAP_GRP_ELEMENT CASCADE CONSTRAINTS; +DROP TABLE TRM_CONCEPT_MAP_GRP_ELM_TGT CASCADE CONSTRAINTS; diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index f9fab301363..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-SNAPSHOT + 3.7.0-SNAPSHOT ../pom.xml @@ -158,7 +158,7 @@ ca.uhn.hapi.fhir hapi-fhir-converter - 3.6.0-SNAPSHOT + 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/TdlDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TdlDstu2Config.java index b71ef68744c..e0d665723de 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TdlDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TdlDstu2Config.java @@ -56,7 +56,7 @@ public class TdlDstu2Config extends BaseJavaConfigDstu2 { return new TdlSecurityInterceptor(); } - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -83,7 +83,7 @@ public class TdlDstu2Config extends BaseJavaConfigDstu2 { return retVal; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); @@ -91,13 +91,11 @@ public class TdlDstu2Config extends BaseJavaConfigDstu2 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu2"); retVal.setDataSource(dataSource()); - retVal.setPackagesToScan("ca.uhn.fhir.jpa.entity"); - retVal.setPersistenceProvider(new HibernatePersistenceProvider()); retVal.setJpaProperties(jpaProperties()); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TdlDstu3Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TdlDstu3Config.java index 8e53ff6a7df..f39210164b9 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TdlDstu3Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TdlDstu3Config.java @@ -43,7 +43,7 @@ public class TdlDstu3Config extends BaseJavaConfigDstu3 { @Value(FHIR_LUCENE_LOCATION_DSTU3) private String myFhirLuceneLocation; - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -71,7 +71,7 @@ public class TdlDstu3Config extends BaseJavaConfigDstu3 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu3"); @@ -151,7 +151,7 @@ public class TdlDstu3Config extends BaseJavaConfigDstu3 { return new SubscriptionsRequireManualActivationInterceptorDstu3(); } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java index c3a16ae5e1d..b5f540d9f0a 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java @@ -54,7 +54,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { return new PublicSecurityInterceptor(); } - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -86,7 +86,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { return retVal; } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); @@ -94,7 +94,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu2"); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java index d5a5b5bf6fd..132a2ed11be 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java @@ -45,7 +45,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { @Value(FHIR_LUCENE_LOCATION_DSTU3) private String myFhirLuceneLocation; - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -95,7 +95,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu3"); @@ -147,7 +147,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { // return new SubscriptionsRequireManualActivationInterceptorDstu3(); // } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); 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..a68663eac28 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 @@ -45,7 +45,7 @@ public class TestR4Config extends BaseJavaConfigR4 { @Value(FHIR_LUCENE_LOCATION_R4) private String myFhirLuceneLocation; - @Bean() + @Bean public DaoConfig daoConfig() { DaoConfig retVal = new DaoConfig(); retVal.setSubscriptionEnabled(true); @@ -88,7 +88,7 @@ public class TestR4Config extends BaseJavaConfigR4 { } @Override - @Bean() + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory(); retVal.setPersistenceUnitName("PU_HapiFhirJpaR4"); @@ -140,7 +140,7 @@ public class TestR4Config extends BaseJavaConfigR4 { return new PublicSecurityInterceptor(); } - @Bean() + @Bean public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager retVal = new JpaTransactionManager(); retVal.setEntityManagerFactory(entityManagerFactory); @@ -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 781c9d34079..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-SNAPSHOT + 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/RestfulResponse.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java index 2bd94a0047e..d56026d2046 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java @@ -21,9 +21,7 @@ package ca.uhn.fhir.rest.server; */ import java.io.IOException; -import java.util.Date; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.util.*; import org.hl7.fhir.instance.model.api.*; @@ -35,7 +33,7 @@ public abstract class RestfulResponse implements IRest private IIdType myOperationResourceId; private IPrimitiveType myOperationResourceLastUpdated; - private ConcurrentHashMap theHeaders = new ConcurrentHashMap(); + private Map> theHeaders = new HashMap<>(); private T theRequestDetails; public RestfulResponse(T requestDetails) { @@ -44,14 +42,14 @@ public abstract class RestfulResponse implements IRest @Override public void addHeader(String headerKey, String headerValue) { - this.getHeaders().put(headerKey, headerValue); + this.getHeaders().computeIfAbsent(headerKey, k -> new ArrayList<>()).add(headerValue); } /** * Get the http headers * @return the headers */ - public ConcurrentHashMap getHeaders() { + public Map> getHeaders() { return theHeaders; } 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..baf83dca891 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 @@ -180,12 +180,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 +570,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 +596,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 +605,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 +623,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); } } @@ -1434,14 +1415,12 @@ public class RestfulServer extends HttpServlet implements IRestfulServer TEXT_ENCODE_ELEMENTS = new HashSet(Arrays.asList("Bundle", "*.text", "*.(mandatory)")); private static Map myFhirContextMap = Collections.synchronizedMap(new HashMap()); + private enum NarrativeModeEnum { + NORMAL, ONLY, SUPPRESS; + + public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { + return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); + } + } + + /** + * Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)} + */ + public static class ResponseEncoding { + private final String myContentType; + private final EncodingEnum myEncoding; + private final Boolean myNonLegacy; + + public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) { + super(); + myEncoding = theEncoding; + myContentType = theContentType; + if (theContentType != null) { + FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); + if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) { + myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1); + } else { + myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1) && !EncodingEnum.isLegacy(theContentType); + } + } else { + FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); + if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)) { + myNonLegacy = null; + } else { + myNonLegacy = Boolean.TRUE; + } + } + } + + public String getContentType() { + return myContentType; + } + + public EncodingEnum getEncoding() { + return myEncoding; + } + + public String getResourceContentType() { + if (Boolean.TRUE.equals(isNonLegacy())) { + return getEncoding().getResourceContentTypeNonLegacy(); + } + return getEncoding().getResourceContentType(); + } + + Boolean isNonLegacy() { + return myNonLegacy; + } + } + public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) { // Pretty print boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails); @@ -272,6 +329,15 @@ public class RestfulServerUtils { * equally, returns thePrefer. */ public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer) { + return determineResponseEncodingNoDefault(theReq, thePrefer, null); + } + + /** + * Try to determing the response content type, given the request Accept header and + * _format parameter. If a value is provided to thePreferContents, we'll + * prefer to return that value over the native FHIR value. + */ + public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer, String thePreferContentType) { String[] format = theReq.getParameters().get(Constants.PARAM_FORMAT); if (format != null) { for (String nextFormat : format) { @@ -333,12 +399,12 @@ public class RestfulServerUtils { ResponseEncoding encoding; if (endSpaceIndex == -1) { if (startSpaceIndex == 0) { - encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken); + encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken, thePreferContentType); } else { - encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex)); + encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex), thePreferContentType); } } else { - encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex, endSpaceIndex)); + encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex, endSpaceIndex), thePreferContentType); String remaining = nextToken.substring(endSpaceIndex + 1); StringTokenizer qualifierTok = new StringTokenizer(remaining, ";"); while (qualifierTok.hasMoreTokens()) { @@ -476,13 +542,18 @@ public class RestfulServerUtils { return context; } - private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType) { + private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType, String thePreferContentType) { EncodingEnum encoding; if (theStrict) { encoding = EncodingEnum.forContentTypeStrict(theContentType); } else { encoding = EncodingEnum.forContentType(theContentType); } + if (isNotBlank(thePreferContentType)) { + if (thePreferContentType.equals(theContentType)) { + return new ResponseEncoding(theFhirContext, encoding, theContentType); + } + } if (encoding == null) { return null; } @@ -749,23 +820,6 @@ public class RestfulServerUtils { return response.sendWriterResponse(theStatusCode, contentType, charset, writer); } - public static String createEtag(String theVersionId) { - return "W/\"" + theVersionId + '"'; - } - - public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) { - String[] retVal = theRequest.getParameters().get(theParamName); - if (retVal == null) { - return null; - } - try { - return Integer.parseInt(retVal[0]); - } catch (NumberFormatException e) { - ourLog.debug("Failed to parse {} value '{}': {}", new Object[] {theParamName, retVal[0], e}); - return null; - } - } - // static Integer tryToExtractNamedParameter(HttpServletRequest theRequest, String name) { // String countString = theRequest.getParameter(name); // Integer count = null; @@ -779,61 +833,27 @@ public class RestfulServerUtils { // return count; // } + public static String createEtag(String theVersionId) { + return "W/\"" + theVersionId + '"'; + } + + public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) { + String[] retVal = theRequest.getParameters().get(theParamName); + if (retVal == null) { + return null; + } + try { + return Integer.parseInt(retVal[0]); + } catch (NumberFormatException e) { + ourLog.debug("Failed to parse {} value '{}': {}", new Object[]{theParamName, retVal[0], e}); + return null; + } + } + public static void validateResourceListNotNull(List theResourceList) { if (theResourceList == null) { throw new InternalErrorException("IBundleProvider returned a null list of resources - This is not allowed"); } } - private enum NarrativeModeEnum { - NORMAL, ONLY, SUPPRESS; - - public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { - return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); - } - } - - /** - * Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)} - */ - public static class ResponseEncoding { - private final EncodingEnum myEncoding; - private final Boolean myNonLegacy; - - public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) { - super(); - myEncoding = theEncoding; - if (theContentType != null) { - FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); - if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) { - myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1); - } else { - myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1) && !EncodingEnum.isLegacy(theContentType); - } - } else { - FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); - if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)) { - myNonLegacy = null; - } else { - myNonLegacy = Boolean.TRUE; - } - } - } - - public EncodingEnum getEncoding() { - return myEncoding; - } - - public String getResourceContentType() { - if (Boolean.TRUE.equals(isNonLegacy())) { - return getEncoding().getResourceContentTypeNonLegacy(); - } - return getEncoding().getResourceContentType(); - } - - public Boolean isNonLegacy() { - return myNonLegacy; - } - } - } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java index 7851a769359..1134843def8 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java @@ -140,7 +140,7 @@ public class ExceptionHandlingInterceptor extends InterceptorAdapter { if (statusCode < 500) { ourLog.warn("Failure during REST processing: {}", theException.toString()); } else { - ourLog.warn("Failure during REST processing: {}", theException); + ourLog.warn("Failure during REST processing", theException); } BaseServerResponseException baseServerResponseException = (BaseServerResponseException) theException; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java index 1b7e81e81ff..1037d5f5234 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java @@ -17,7 +17,8 @@ import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.hl7.fhir.instance.model.api.IBaseBinary; import org.hl7.fhir.instance.model.api.IBaseResource; import javax.servlet.ServletException; @@ -371,7 +372,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { /* * Not binary */ - if (!force && "Binary".equals(theRequestDetails.getResourceName())) { + if (!force && (theResponseObject.getResponseResource() instanceof IBaseBinary)) { return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java new file mode 100644 index 00000000000..27e70452887 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java @@ -0,0 +1,133 @@ +package ca.uhn.fhir.rest.server.interceptor; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.rest.api.Constants; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +/** + * This interceptor allows a client to request that a Media resource be + * served as the raw contents of the resource, assuming either: + *
      + *
    • The client explicitly requests the correct content type using the Accept header
    • + *
    • The client explicitly requests raw output by adding the parameter _output=data
    • + *
    + */ +public class ServeMediaResourceRawInterceptor extends InterceptorAdapter { + + public static final String MEDIA_CONTENT_CONTENT_TYPE_OPT = "Media.content.contentType"; + + private static final Set RESPOND_TO_OPERATION_TYPES; + + static { + Set respondToOperationTypes = new HashSet<>(); + respondToOperationTypes.add(RestOperationTypeEnum.READ); + respondToOperationTypes.add(RestOperationTypeEnum.VREAD); + RESPOND_TO_OPERATION_TYPES = Collections.unmodifiableSet(respondToOperationTypes); + } + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { + if (theResponseObject == null) { + return true; + } + + + FhirContext context = theRequestDetails.getFhirContext(); + String resourceName = context.getResourceDefinition(theResponseObject).getName(); + + // Are we serving a FHIR read request on the Media resource type + if (!"Media".equals(resourceName) || !RESPOND_TO_OPERATION_TYPES.contains(theRequestDetails.getRestOperationType())) { + return true; + } + + // What is the content type of the Media resource we're returning? + String contentType = null; + Optional contentTypeOpt = context.newFluentPath().evaluateFirst(theResponseObject, MEDIA_CONTENT_CONTENT_TYPE_OPT, IPrimitiveType.class); + if (contentTypeOpt.isPresent()) { + contentType = contentTypeOpt.get().getValueAsString(); + } + + // What is the data of the Media resource we're returning? + IPrimitiveType data = null; + Optional dataOpt = context.newFluentPath().evaluateFirst(theResponseObject, "Media.content.data", IPrimitiveType.class); + if (dataOpt.isPresent()) { + data = dataOpt.get(); + } + + if (isBlank(contentType) || data == null) { + return true; + } + + RestfulServerUtils.ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, null, contentType); + if (responseEncoding != null) { + if (contentType.equals(responseEncoding.getContentType())) { + returnRawResponse(theRequestDetails, theServletResponse, contentType, data); + return false; + + } + } + + String[] outputParam = theRequestDetails.getParameters().get("_output"); + if (outputParam != null && "data".equals(outputParam[0])) { + returnRawResponse(theRequestDetails, theServletResponse, contentType, data); + return false; + } + + return true; + } + + private void returnRawResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, String theContentType, IPrimitiveType theData) { + theServletResponse.setStatus(200); + if (theRequestDetails.getServer() instanceof RestfulServer) { + RestfulServer rs = (RestfulServer) theRequestDetails.getServer(); + rs.addHeadersToResponse(theServletResponse); + } + + theServletResponse.addHeader(Constants.HEADER_CONTENT_TYPE, theContentType); + + // Write the response + try { + theServletResponse.getOutputStream().write(theData.getValue()); + theServletResponse.getOutputStream().close(); + } catch (IOException e) { + throw new InternalErrorException(e); + } + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java index ace45066ee5..4cc33ecbc98 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java @@ -87,7 +87,7 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter return; } - handleDeny(decision); + handleDeny(theRequestDetails, decision); } @Override @@ -185,6 +185,9 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter // Nothing yet return OperationExamineDirection.NONE; + case GRAPHQL_REQUEST: + return OperationExamineDirection.IN; + default: // Should not happen throw new IllegalStateException("Unable to apply security to event of type " + theOperation); @@ -219,6 +222,19 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter return Collections.unmodifiableSet(myFlags); } + /** + * This property configures any flags affecting how authorization is + * applied. By default no flags are applied. + * + * @param theFlags The flags (must not be null) + * @see #setFlags(AuthorizationFlagsEnum...) + */ + public AuthorizationInterceptor setFlags(Collection theFlags) { + Validate.notNull(theFlags, "theFlags must not be null"); + myFlags = new HashSet<>(theFlags); + return this; + } + /** * This property configures any flags affecting how authorization is * applied. By default no flags are applied. @@ -238,6 +254,17 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter * throw {@link ForbiddenOperationException} (HTTP 403) with error message citing the * rule name which trigered failure *

    + * + * @since HAPI FHIR 3.6.0 + */ + protected void handleDeny(RequestDetails theRequestDetails, Verdict decision) { + handleDeny(decision); + } + + /** + * This method should not be overridden. As of HAPI FHIR 3.6.0, you + * should override {@link #handleDeny(RequestDetails, Verdict)} instead. This + * method will be removed in the future. */ protected void handleDeny(Verdict decision) { if (decision.getDecidingRule() != null) { @@ -350,51 +377,6 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter handleUserOperation(theRequest, theNewResource, RestOperationTypeEnum.UPDATE); } - /** - * This property configures any flags affecting how authorization is - * applied. By default no flags are applied. - * - * @param theFlags The flags (must not be null) - * @see #setFlags(AuthorizationFlagsEnum...) - */ - public AuthorizationInterceptor setFlags(Collection theFlags) { - Validate.notNull(theFlags, "theFlags must not be null"); - myFlags = new HashSet<>(theFlags); - return this; - } - - private static UnsupportedOperationException failForDstu1() { - return new UnsupportedOperationException("Use of this interceptor on DSTU1 servers is not supportd"); - } - - static List toListOfResourcesAndExcludeContainer(IBaseResource theResponseObject, FhirContext fhirContext) { - if (theResponseObject == null) { - return Collections.emptyList(); - } - - List retVal; - - boolean isContainer = false; - if (theResponseObject instanceof IBaseBundle) { - isContainer = true; - } else if (theResponseObject instanceof IBaseParameters) { - isContainer = true; - } - - if (!isContainer) { - return Collections.singletonList(theResponseObject); - } - - retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class); - - // Exclude the container - if (retVal.size() > 0 && retVal.get(0) == theResponseObject) { - retVal = retVal.subList(1, retVal.size()); - } - - return retVal; - } - private enum OperationExamineDirection { BOTH, IN, @@ -432,4 +414,36 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter } + private static UnsupportedOperationException failForDstu1() { + return new UnsupportedOperationException("Use of this interceptor on DSTU1 servers is not supportd"); + } + + static List toListOfResourcesAndExcludeContainer(IBaseResource theResponseObject, FhirContext fhirContext) { + if (theResponseObject == null) { + return Collections.emptyList(); + } + + List retVal; + + boolean isContainer = false; + if (theResponseObject instanceof IBaseBundle) { + isContainer = true; + } else if (theResponseObject instanceof IBaseParameters) { + isContainer = true; + } + + if (!isContainer) { + return Collections.singletonList(theResponseObject); + } + + retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class); + + // Exclude the container + if (retVal.size() > 0 && retVal.get(0) == theResponseObject) { + retVal = retVal.subList(1, retVal.size()); + } + + return retVal; + } + } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java index bdb381ae088..f1ee4447bbe 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java @@ -108,5 +108,4 @@ abstract class BaseRule implements IAuthRule { Verdict newVerdict() { return new Verdict(myMode, this); } - } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderGraphQL.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderGraphQL.java new file mode 100644 index 00000000000..1e7ea89a0dd --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderGraphQL.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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 interface IAuthRuleBuilderGraphQL { + + /** + * Note that this is an all-or-nothing grant for now, it + * is not yet possible to specify individual resource security when + * using GraphQL. + */ + IAuthRuleFinished any(); +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperationNamed.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperationNamed.java index d2ff8349d76..93dbc550635 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperationNamed.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperationNamed.java @@ -28,36 +28,36 @@ public interface IAuthRuleBuilderOperationNamed { /** * Rule applies to invocations of this operation at the server 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..c22bff128fa --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperationNamedAndScoped.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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 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..32c555e1c39 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 @@ -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/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 66044200232..d5a81fb6e91 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -84,6 +84,8 @@ public abstract class BaseMethodBinding { } } + // This allows us to invoke methods on private classes + myMethod.setAccessible(true); } protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List> thePreferTypes) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java index 3de8c16a612..82cf98d41a5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java @@ -19,7 +19,6 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ReflectionUtil; import ca.uhn.fhir.util.UrlUtil; @@ -57,27 +56,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { - protected static final Set ALLOWED_PARAMS; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class); - static { - HashSet set = new HashSet(); - set.add(Constants.PARAM_FORMAT); - set.add(Constants.PARAM_NARRATIVE); - set.add(Constants.PARAM_PRETTY); - set.add(Constants.PARAM_SORT); - set.add(Constants.PARAM_SORT_ASC); - set.add(Constants.PARAM_SORT_DESC); - set.add(Constants.PARAM_COUNT); - set.add(Constants.PARAM_SUMMARY); - set.add(Constants.PARAM_ELEMENTS); - set.add(ResponseHighlighterInterceptor.PARAM_RAW); - ALLOWED_PARAMS = Collections.unmodifiableSet(set); - } - private MethodReturnTypeEnum myMethodReturnType; private String myResourceName; - private Class myResourceType; @SuppressWarnings("unchecked") public BaseResourceReturningMethodBinding(Class theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { @@ -112,11 +94,12 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi if (theReturnResourceType != null) { if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) { - if (Modifier.isAbstract(theReturnResourceType.getModifiers()) || Modifier.isInterface(theReturnResourceType.getModifiers())) { - // If we're returning an abstract type, that's ok - } else { - myResourceType = (Class) theReturnResourceType; - myResourceName = theContext.getResourceDefinition(myResourceType).getName(); + + // If we're returning an abstract type, that's ok, but if we know the resource + // type let's grab it + if (!Modifier.isAbstract(theReturnResourceType.getModifiers()) && !Modifier.isInterface(theReturnResourceType.getModifiers())) { + Class resourceType = (Class) theReturnResourceType; + myResourceName = theContext.getResourceDefinition(resourceType).getName(); } } } @@ -215,11 +198,9 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi linkPrev = RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theResult.getPreviousPageId(), theRequest.getParameters(), prettyPrint, theBundleType); } } else if (searchId != null) { - int offset = theOffset + resourceList.size(); - // We're doing offset pages - if (numTotalResults == null || offset < numTotalResults) { - linkNext = (RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, offset, numToReturn, theRequest.getParameters(), prettyPrint, theBundleType)); + if (numTotalResults == null || theOffset + numToReturn < numTotalResults) { + linkNext = (RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theOffset + numToReturn, numToReturn, theRequest.getParameters(), prettyPrint, theBundleType)); } if (theOffset > 0) { int start = Math.max(0, theOffset - theLimit); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index ed9ccaabd1a..511abb07e34 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -202,6 +202,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { @Override public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) { + if (isBlank(theRequest.getOperation())) { + return false; + } + if (!myName.equals(theRequest.getOperation())) { if (!myName.equals(WILDCARD_NAME)) { return false; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java index 9d3eb9229b8..dacce858677 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java @@ -110,7 +110,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding { return false; } for (String next : theRequest.getParameters().keySet()) { - if (!ALLOWED_PARAMS.contains(next)) { + if (!next.startsWith("_")) { return false; } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java index 3b06a9b2f19..ae26f2c1927 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java @@ -23,12 +23,10 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.ConfigurationException; @@ -52,12 +50,20 @@ import javax.annotation.Nonnull; public class SearchMethodBinding extends BaseResourceReturningMethodBinding { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class); + private static final Set SPECIAL_SEARCH_PARAMS; private String myCompartmentName; private String myDescription; private Integer myIdParamIndex; private String myQueryName; private boolean myAllowUnknownParams; + static { + HashSet specialSearchParams = new HashSet<>(); + specialSearchParams.add(IAnyResource.SP_RES_ID); + specialSearchParams.add(IAnyResource.SP_RES_LANGUAGE); + SPECIAL_SEARCH_PARAMS = Collections.unmodifiableSet(specialSearchParams); + } + public SearchMethodBinding(Class theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { super(theReturnResourceType, theMethod, theContext, theProvider); Search search = theMethod.getAnnotation(Search.class); @@ -75,27 +81,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } } - /* - * Check for parameter combinations and names that are invalid - */ - List parameters = getParameters(); - for (int i = 0; i < parameters.size(); i++) { - IParameter next = parameters.get(i); - if (!(next instanceof SearchParameter)) { - continue; - } - - SearchParameter sp = (SearchParameter) next; - if (sp.getName().startsWith("_")) { - if (ALLOWED_PARAMS.contains(sp.getName())) { - String msg = getContext().getLocalizer().getMessage(getClass().getName() + ".invalidSpecialParamName", theMethod.getName(), theMethod.getDeclaringClass().getSimpleName(), - sp.getName()); - throw new ConfigurationException(msg); - } - } - - } - /* * Only compartment searching methods may have an ID parameter */ @@ -232,7 +217,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } } for (String next : theRequest.getParameters().keySet()) { - if (ALLOWED_PARAMS.contains(next)) { + if (next.startsWith("_") && !SPECIAL_SEARCH_PARAMS.contains(next)) { methodParamsTemp.add(next); } } 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 3ffb3d9b9f1..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 @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.util.List; import java.util.Map.Entry; import java.util.zip.GZIPOutputStream; @@ -49,6 +50,7 @@ public class ServletRestfulResponse extends RestfulResponse header : getHeaders().entrySet()) { - theHttpResponse.setHeader(header.getKey(), header.getValue()); + for (Entry> header : getHeaders().entrySet()) { + final String key = header.getKey(); + boolean first = true; + for (String value : header.getValue()) { + // existing headers should be overridden + if (first) { + theHttpResponse.setHeader(key, value); + first = false; + } else { + theHttpResponse.addHeader(key, value); + } + } } } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java new file mode 100644 index 00000000000..009dbe3af5b --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.rest.server; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.MockSettings; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Unit tests of {@link RestfulResponse}. + */ +public class RestfulResponseTest { + @Test + public void addMultipleHeaderValues() { + @SuppressWarnings("unchecked") + final RestfulResponse restfulResponse = + mock(RestfulResponse.class, withSettings() + .useConstructor((RequestDetails) null).defaultAnswer(CALLS_REAL_METHODS)); + + restfulResponse.addHeader("Authorization", "Basic"); + restfulResponse.addHeader("Authorization", "Bearer"); + restfulResponse.addHeader("Cache-Control", "no-cache, no-store"); + + assertEquals(2, restfulResponse.getHeaders().size()); + assertThat(restfulResponse.getHeaders().get("Authorization"), Matchers.contains("Basic", "Bearer")); + assertThat(restfulResponse.getHeaders().get("Cache-Control"), Matchers.contains("no-cache, no-store")); + } +} diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java new file mode 100644 index 00000000000..f56e475f7a2 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java @@ -0,0 +1,65 @@ +package ca.uhn.fhir.rest.server.servlet; + +import ca.uhn.fhir.rest.server.RestfulServer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * Unit tests of {@link ServletRestfulResponse}. + */ +public class ServletRestfulResponseTest { + @Mock + private RestfulServer server; + + @Mock + private ServletOutputStream servletOutputStream; + + @Mock + private HttpServletResponse servletResponse; + + private ServletRequestDetails requestDetails; + + private ServletRestfulResponse response; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Before + public void init() throws IOException { + Mockito.when(servletResponse.getOutputStream()).thenReturn(servletOutputStream); + + requestDetails = new ServletRequestDetails(); + requestDetails.setServer(server); + requestDetails.setServletResponse(servletResponse); + response = new ServletRestfulResponse(requestDetails); + } + + @Test + public void addMultipleHeaderValues() throws IOException { + final ServletRestfulResponse response = new ServletRestfulResponse(requestDetails); + response.addHeader("Authorization", "Basic"); + response.addHeader("Authorization", "Bearer"); + response.addHeader("Cache-Control", "no-cache, no-store"); + + response.getResponseWriter(200, "Status", "text/plain", "UTF-8", false); + + final InOrder orderVerifier = Mockito.inOrder(servletResponse); + orderVerifier.verify(servletResponse).setHeader(eq("Authorization"), eq("Basic")); + orderVerifier.verify(servletResponse).addHeader(eq("Authorization"), eq("Bearer")); + verify(servletResponse).setHeader(eq("Cache-Control"), eq("no-cache, no-store")); + } +} diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index 504292423cf..0a89d730295 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java index 7cba8a04df5..cf8d2d42f3a 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2; import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; import ca.uhn.fhir.jpa.config.BaseJavaConfigR4; import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.provider.BaseJpaProvider; import ca.uhn.fhir.jpa.provider.BaseJpaSystemProvider; import ca.uhn.fhir.okhttp.client.OkHttpRestfulClientFactory; @@ -180,7 +181,7 @@ public class FhirAutoConfiguration { } @Configuration - @EntityScan("ca.uhn.fhir.jpa.entity") + @EntityScan(basePackages = {"ca.uhn.fhir.jpa.entity", "ca.uhn.fhir.jpa.model.entity"}) @EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data") static class FhirJpaDaoConfiguration { @@ -192,6 +193,12 @@ public class FhirAutoConfiguration { return fhirDaoConfig; } + @Bean + @ConditionalOnMissingBean + @ConfigurationProperties("hapi.fhir.jpa") + public ModelConfig fhirModelConfig() { + return fhirDaoConfig().getModelConfig(); + } } @Configuration 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 cea8270bc0b..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-SNAPSHOT + 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 deb457a1562..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-SNAPSHOT + 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 efb192c60db..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-SNAPSHOT + 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 63f8c15708d..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-SNAPSHOT + 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 3e3e2860fff..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-SNAPSHOT + 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 3dd8c637d71..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-SNAPSHOT + 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 f1c9cb1577a..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-SNAPSHOT + 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 d9764cf1888..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-SNAPSHOT + 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 57f3736e620..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-SNAPSHOT + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java index 2c618758489..f6d88a86b8e 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java @@ -2651,6 +2651,11 @@ public class GenericClientDstu2Test { // nothing } + @Override + public void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle) { + // nothing + } + @Override public EncodingEnum getEncoding() { // TODO Auto-generated method stub 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 1de2fe121a4..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-SNAPSHOT + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java index ee8baaccde0..11da788d945 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java @@ -11,6 +11,7 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; import java.util.List; +import java.util.Optional; public class FluentPathDstu3 implements IFluentPath { @@ -43,4 +44,9 @@ public class FluentPathDstu3 implements IFluentPath { return (List) result; } + @Override + public Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType) { + return evaluate(theInput, thePath, theReturnType).stream().findFirst(); + } + } 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/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java index 62b9f81d527..aa427a35a67 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java @@ -5,7 +5,12 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.map.LazyMap; import org.hamcrest.core.StringContains; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.Coding; @@ -28,6 +33,8 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.thymeleaf.messageresolver.StandardMessageResolver; +import org.thymeleaf.templateresource.ITemplateResource; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.DataFormatException; @@ -77,6 +84,52 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { } + @Test + public void testTranslations() throws DataFormatException { + CustomThymeleafNarrativeGenerator customGen = new CustomThymeleafNarrativeGenerator("classpath:/testnarrative.properties"); + customGen.setIgnoreFailures(false); + customGen.setIgnoreMissingTemplates(false); + + FhirContext ctx = FhirContext.forDstu3(); + ctx.setNarrativeGenerator(customGen); + + Patient value = new Patient(); + + value.addIdentifier().setSystem("urn:names").setValue("123456"); + value.addName().setFamily("blow").addGiven("joe").addGiven((String) null).addGiven("john"); + //@formatter:off + value.addAddress() + .addLine("123 Fake Street").addLine("Unit 1") + .setCity("Toronto").setState("ON").setCountry("Canada"); + //@formatter:on + + value.setBirthDate(new Date()); + + Transformer transformer = new Transformer() { + + @Override + public Object transform(Object input) { + return "UNTRANSLATED:" + input; + }}; + + Map translations = new HashMap<>(); + translations.put("some_text", "Some beautiful proze"); + + customGen.setMessageResolver(new StandardMessageResolver() { + @Override + protected Map resolveMessagesForTemplate(String template, + ITemplateResource templateResource, Locale locale) { + return LazyMap.decorate(translations, transformer); + } + }); + + Narrative narrative = new Narrative(); + customGen.generateNarrative(ctx, value, narrative); + String output = narrative.getDiv().getValueAsString(); + ourLog.info(output); + assertThat(output, StringContains.containsString("Some beautiful proze")); + assertThat(output, StringContains.containsString("UNTRANSLATED:other_text")); + } @Test public void testGenerateDiagnosticReport() throws DataFormatException { 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/ServerExceptionDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerExceptionDstu3Test.java index 70c3f0e4e9b..25ef830c6a9 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerExceptionDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerExceptionDstu3Test.java @@ -1,13 +1,14 @@ package ca.uhn.fhir.rest.server; -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.concurrent.TimeUnit; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +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.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -25,32 +26,33 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.annotation.Search; -import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.util.PortUtil; -import ca.uhn.fhir.util.TestUtil; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.contains; public class ServerExceptionDstu3Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerExceptionDstu3Test.class); + public static BaseServerResponseException ourException; private static CloseableHttpClient ourClient; private static FhirContext ourCtx = FhirContext.forDstu3(); - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerExceptionDstu3Test.class); private static int ourPort; private static Server ourServer; - public static BaseServerResponseException ourException; @Test public void testAddHeadersNotFound() throws Exception { - + OperationOutcome operationOutcome = new OperationOutcome(); operationOutcome.addIssue().setCode(IssueType.BUSINESSRULE); - + ourException = new ResourceNotFoundException("SOME MESSAGE"); ourException.addResponseHeader("X-Foo", "BAR BAR"); - + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient"); CloseableHttpResponse status = ourClient.execute(httpGet); @@ -58,7 +60,7 @@ public class ServerExceptionDstu3Test { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(status.getStatusLine().toString()); ourLog.info(responseContent); - + assertEquals(404, status.getStatusLine().getStatusCode()); assertEquals("BAR BAR", status.getFirstHeader("X-Foo").getValue()); assertThat(status.getFirstHeader("X-Powered-By").getValue(), containsString("HAPI FHIR")); @@ -68,25 +70,59 @@ public class ServerExceptionDstu3Test { } + @Test + public void testResponseUsesCorrectEncoding() throws Exception { + + OperationOutcome operationOutcome = new OperationOutcome(); + operationOutcome + .addIssue() + .setCode(IssueType.PROCESSING) + .setSeverity(OperationOutcome.IssueSeverity.ERROR) + .setDiagnostics("El nombre está vacío"); + + ourException = new InternalErrorException("Error", operationOutcome); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=json"); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + byte[] responseContentBytes = IOUtils.toByteArray(status.getEntity().getContent()); + String responseContent = new String(responseContentBytes, Charsets.UTF_8); + ourLog.info(status.getStatusLine().toString()); + ourLog.info(responseContent); + assertThat(responseContent, containsString("El nombre está vacío")); + } + + } + @Test public void testAuthorize() throws Exception { - + OperationOutcome operationOutcome = new OperationOutcome(); operationOutcome.addIssue().setCode(IssueType.BUSINESSRULE); - + ourException = new AuthenticationException().addAuthenticateHeaderForRealm("REALM"); - + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient"); - CloseableHttpResponse status = ourClient.execute(httpGet); - try { + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(status.getStatusLine().toString()); ourLog.info(responseContent); - + assertEquals(401, status.getStatusLine().getStatusCode()); assertEquals("Basic realm=\"REALM\"", status.getFirstHeader("WWW-Authenticate").getValue()); - } finally { - IOUtils.closeQuietly(status.getEntity().getContent()); + } + + } + + public static class DummyPatientResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Search() + public List search() { + throw ourException; } } @@ -121,18 +157,4 @@ public class ServerExceptionDstu3Test { } - public static class DummyPatientResourceProvider implements IResourceProvider { - - @Override - public Class getResourceType() { - return Patient.class; - } - - @Search() - public List search() { - throw ourException; - } - - } - } 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-dstu3/src/test/java/ca/uhn/fhir/util/FhirTerserDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/FhirTerserDstu3Test.java index b8cece751d7..f5344eb0a73 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/FhirTerserDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/FhirTerserDstu3Test.java @@ -27,6 +27,35 @@ public class FhirTerserDstu3Test { private static FhirContext ourCtx = FhirContext.forDstu3(); + @Test + public void testCloneIntoBundle() { + Bundle input = new Bundle(); + input.setType(Bundle.BundleType.TRANSACTION); + + Patient pt = new Patient(); + pt.setId("pt"); + pt.setActive(true); + input + .addEntry() + .setResource(pt) + .getRequest() + .setUrl("Patient/pt") + .setMethod(Bundle.HTTPVerb.PUT); + + Observation obs = new Observation(); + obs.setId("obs"); + obs.getSubject().setReference("Patient/pt"); + input + .addEntry() + .setResource(obs) + .getRequest() + .setUrl("Observation/obs") + .setMethod(Bundle.HTTPVerb.PUT); + + Bundle ionputClone = new Bundle(); + ourCtx.newTerser().cloneInto(input, ionputClone, false); + } + @Test public void testCloneIntoComposite() { Quantity source = new Quantity(); diff --git a/hapi-fhir-structures-dstu3/src/test/resources/TestPatient.html b/hapi-fhir-structures-dstu3/src/test/resources/TestPatient.html new file mode 100644 index 00000000000..88d60488a3e --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/TestPatient.html @@ -0,0 +1,4 @@ +
    +

    Some Text

    +

    Some Text

    +
    diff --git a/hapi-fhir-structures-dstu3/src/test/resources/testnarrative.properties b/hapi-fhir-structures-dstu3/src/test/resources/testnarrative.properties new file mode 100644 index 00000000000..22f3b3c51b0 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/testnarrative.properties @@ -0,0 +1,2 @@ +patient.class=org.hl7.fhir.dstu3.model.Patient +patient.narrative=classpath:/TestPatient.html diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index e8955e2a052..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-SNAPSHOT + 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 148068bcb4e..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-SNAPSHOT + 3.7.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java index ae5dcbba6a7..45e9aa7e27f 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java @@ -1,7 +1,8 @@ package org.hl7.fhir.r4.hapi.fluentpath; -import java.util.List; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.fluentpath.FluentPathExecutionException; +import ca.uhn.fhir.fluentpath.IFluentPath; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; @@ -9,39 +10,44 @@ import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.utils.FHIRPathEngine; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.fluentpath.FluentPathExecutionException; -import ca.uhn.fhir.fluentpath.IFluentPath; +import java.util.List; +import java.util.Optional; -public class FluentPathR4 implements IFluentPath { +public class FluentPathR4 implements IFluentPath { - private FHIRPathEngine myEngine; + private FHIRPathEngine myEngine; - public FluentPathR4(FhirContext theCtx) { - if (!(theCtx.getValidationSupport() instanceof IValidationSupport)) { - throw new IllegalStateException("Validation support module configured on context appears to be for the wrong FHIR version- Does not extend " + IValidationSupport.class.getName()); - } - IValidationSupport validationSupport = (IValidationSupport) theCtx.getValidationSupport(); - myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport)); - } + public FluentPathR4(FhirContext theCtx) { + if (!(theCtx.getValidationSupport() instanceof IValidationSupport)) { + throw new IllegalStateException("Validation support module configured on context appears to be for the wrong FHIR version- Does not extend " + IValidationSupport.class.getName()); + } + IValidationSupport validationSupport = (IValidationSupport) theCtx.getValidationSupport(); + myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport)); + } - @SuppressWarnings("unchecked") - @Override - public List evaluate(IBase theInput, String thePath, Class theReturnType) { - List result; - try { - result = myEngine.evaluate((Base)theInput, thePath); - } catch (FHIRException e) { - throw new FluentPathExecutionException(e); - } + @SuppressWarnings("unchecked") + @Override + public List evaluate(IBase theInput, String thePath, Class theReturnType) { + List result; + try { + result = myEngine.evaluate((Base) theInput, thePath); + } catch (FHIRException e) { + throw new FluentPathExecutionException(e); + } + + for (Base next : result) { + if (!theReturnType.isAssignableFrom(next.getClass())) { + throw new FluentPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName()); + } + } + + return (List) result; + } + + @Override + public Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType) { + return evaluate(theInput, thePath, theReturnType).stream().findFirst(); + } - for (Base next : result) { - if (!theReturnType.isAssignableFrom(next.getClass())) { - throw new FluentPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName()); - } - } - - return (List) result; - } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientHeadersR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientHeadersR4Test.java index 9d4fdf78a12..fc690862fdd 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientHeadersR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientHeadersR4Test.java @@ -1,17 +1,13 @@ package ca.uhn.fhir.rest.client; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.util.RandomServerPortProvider; import ca.uhn.fhir.util.TestUtil; -import ca.uhn.fhir.util.VersionUtil; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpUriRequest; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; @@ -20,9 +16,7 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.mockito.ArgumentCaptor; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -40,8 +34,9 @@ public class ClientHeadersR4Test { private static Server ourServer; private static String ourServerBase; private static HashMap> ourHeaders; - private static IGenericClient ourClient; + private static HashMap ourParams; private static String ourMethod; + private IGenericClient myClient; @Before public void before() { @@ -49,34 +44,125 @@ public class ClientHeadersR4Test { ourMethod = null; } - private String expectedUserAgent() { - return "HAPI-FHIR/" + VersionUtil.getVersion() + " (FHIR Client; FHIR " + FhirVersionEnum.R4.getFhirVersionString() + "/R4; apache)"; - } - - private byte[] extractBodyAsByteArray(ArgumentCaptor capt) throws IOException { - byte[] body = IOUtils.toByteArray(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent()); - return body; - } - - private String extractBodyAsString(ArgumentCaptor capt) throws IOException { - String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent(), "UTF-8"); - return body; - } - @Test - public void testCreateWithPreferRepresentationServerReturnsResource() throws Exception { + public void testReadXml() { + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedXml() + .execute(); + + assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals("xml", ourParams.get(Constants.PARAM_FORMAT)[0]); + } + + @Test + public void testReadXmlNoParam() { + myClient.setFormatParamStyle(RequestFormatParamStyleEnum.NONE); + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedXml() + .execute(); + + assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals(null, ourParams.get(Constants.PARAM_FORMAT)); + } + + @Test + public void testReadJson() { + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedJson() + .execute(); + + assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals("json", ourParams.get(Constants.PARAM_FORMAT)[0]); + } + + @Test + public void testReadJsonNoParam() { + myClient.setFormatParamStyle(RequestFormatParamStyleEnum.NONE); + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedJson() + .execute(); + + assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals(null, ourParams.get(Constants.PARAM_FORMAT)); + } + + @Test + public void testReadXmlDisable() { + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedXml() + .execute(); + + assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals("xml", ourParams.get(Constants.PARAM_FORMAT)[0]); + } + + @Test + public void testCreateWithPreferRepresentationServerReturnsResource() { final Patient resp1 = new Patient(); resp1.setActive(true); - MethodOutcome resp = ourClient.create().resource(resp1).execute(); + MethodOutcome resp = myClient.create().resource(resp1).execute(); assertNotNull(resp); assertEquals(1, ourHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); assertEquals("application/fhir+xml; charset=UTF-8", ourHeaders.get(Constants.HEADER_CONTENT_TYPE).get(0)); } + @Before + public void beforeCreateClient() { + myClient = ourCtx.newRestfulGenericClient(ourServerBase); + } + + private static class TestServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (ourHeaders != null) { + fail(); + } + ourHeaders = new HashMap<>(); + ourParams = new HashMap<>(req.getParameterMap()); + ourMethod = req.getMethod(); + Enumeration names = req.getHeaderNames(); + while (names.hasMoreElements()) { + String nextName = names.nextElement(); + ourHeaders.put(nextName, new ArrayList<>()); + Enumeration values = req.getHeaders(nextName); + while (values.hasMoreElements()) { + ourHeaders.get(nextName).add(values.nextElement()); + } + } + + resp.setStatus(200); + + if (req.getMethod().equals("GET")) { + resp.setContentType("application/json"); + resp.getWriter().append("{\"resourceType\":\"Patient\"}"); + resp.getWriter().close(); + } + + } + + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); @@ -94,7 +180,6 @@ public class ClientHeadersR4Test { ourServerBase = "http://localhost:" + myPort + "/fhir/context"; ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); - ourClient = ourCtx.newRestfulGenericClient(ourServerBase); ServletHolder servletHolder = new ServletHolder(); servletHolder.setServlet(new TestServlet()); @@ -105,29 +190,4 @@ public class ClientHeadersR4Test { } - private static class TestServlet extends HttpServlet { - - @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - - if (ourHeaders != null) { - fail(); - } - ourHeaders = new HashMap<>(); - ourMethod = req.getMethod(); - Enumeration names = req.getHeaderNames(); - while (names.hasMoreElements()) { - String nextName = names.nextElement(); - ourHeaders.put(nextName, new ArrayList()); - Enumeration values = req.getHeaders(nextName); - while (values.hasMoreElements()) { - ourHeaders.get(nextName).add(values.nextElement()); - } - } - - resp.setStatus(200); - } - - } - } 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/client/OperationClientR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java index 94d686d93da..6f658fe5ec6 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java @@ -1,41 +1,45 @@ package ca.uhn.fhir.rest.client; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.ReaderInputStream; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.StringType; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.io.InputStream; import java.io.StringReader; import java.nio.charset.Charset; import java.util.List; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.input.ReaderInputStream; -import org.apache.http.HttpResponse; -import org.apache.http.ProtocolVersion; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.*; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicStatusLine; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.StringType; -import org.junit.*; -import org.mockito.ArgumentCaptor; -import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import com.google.common.base.Charsets; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationParam; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.client.api.*; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.util.TestUtil; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class OperationClientR4Test { @@ -48,13 +52,6 @@ public class OperationClientR4Test { private ArgumentCaptor capt; private IGenericClient ourGenClient; - - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - - @Before public void before() throws Exception { ourCtx = FhirContext.forR4(); @@ -64,7 +61,7 @@ public class OperationClientR4Test { ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); - + Parameters outParams = new Parameters(); outParams.addParameter().setName("FOO"); final String retVal = ourCtx.newXmlParser().encodeResourceToString(outParams); @@ -75,7 +72,7 @@ public class OperationClientR4Test { when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); when(ourHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { @Override - public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + public InputStream answer(InvocationOnMock theInvocation) { return new ReaderInputStream(new StringReader(retVal), Charset.forName("UTF-8")); } }); @@ -95,10 +92,10 @@ public class OperationClientR4Test { .execute(); Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); assertEquals("FOO", response.getParameter().get(0).getName()); - + HttpPost value = (HttpPost) capt.getAllValues().get(0); - String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent(), Charsets.UTF_8); - IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent()); + String requestBody = IOUtils.toString(value.getEntity().getContent(), Charsets.UTF_8); + IOUtils.closeQuietly(value.getEntity().getContent()); ourLog.info(requestBody); Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody); assertEquals("http://foo/$nonrepeating", value.getURI().toASCIIString()); @@ -110,7 +107,7 @@ public class OperationClientR4Test { } @Test - public void testNonRepeatingGenericUsingUrl() throws Exception { + public void testNonRepeatingGenericUsingUrl() { ourGenClient .operation() .onServer() @@ -121,14 +118,31 @@ public class OperationClientR4Test { .execute(); Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); assertEquals("FOO", response.getParameter().get(0).getName()); - + HttpGet value = (HttpGet) capt.getAllValues().get(0); assertEquals("http://foo/$nonrepeating?valstr=str&valtok=sys2%7Cval2", value.getURI().toASCIIString()); } + @Test + public void testNonRepeatingGenericUsingUrl2() { + ourGenClient + .operation() + .onServer() + .named("nonrepeating") + .withParameters(new Parameters()) + .andSearchParameter("valstr", new StringParam("str")) + .andSearchParameter("valtok", new TokenParam("sys2", "val2")) + .useHttpGet() + .execute(); + Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); + assertEquals("FOO", response.getParameter().get(0).getName()); + + HttpGet value = (HttpGet) capt.getAllValues().get(0); + assertEquals("http://foo/$nonrepeating?valstr=str&valtok=sys2%7Cval2", value.getURI().toASCIIString()); + } @Test - public void testOperationOnInstanceWithIncompleteInstanceId() throws Exception { + public void testOperationOnInstanceWithIncompleteInstanceId() { try { ourGenClient .operation() @@ -146,10 +160,10 @@ public class OperationClientR4Test { public void testNonRepeatingUsingParameters() throws Exception { Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); assertEquals("FOO", response.getParameter().get(0).getName()); - + HttpPost value = (HttpPost) capt.getAllValues().get(0); - String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent(), Charsets.UTF_8); - IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent()); + String requestBody = IOUtils.toString(value.getEntity().getContent(), Charsets.UTF_8); + IOUtils.closeQuietly(value.getEntity().getContent()); ourLog.info(requestBody); Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody); assertEquals("http://foo/$nonrepeating", value.getURI().toASCIIString()); @@ -160,48 +174,52 @@ public class OperationClientR4Test { assertEquals("sys|val", ((StringType) request.getParameter().get(1).getValue()).getValue()); } - - public interface IOpClient extends IBasicClient { + public interface IOpClient extends IBasicClient { @Operation(name = "$andlist", idempotent = true) - public Parameters andlist( - //@formatter:off - @OperationParam(name="valstr", max=10) StringAndListParam theValStr, - @OperationParam(name="valtok", max=10) TokenAndListParam theValTok - //@formatter:on + Parameters andlist( + //@formatter:off + @OperationParam(name = "valstr", max = 10) StringAndListParam theValStr, + @OperationParam(name = "valtok", max = 10) TokenAndListParam theValTok + //@formatter:on ); @Operation(name = "$andlist-withnomax", idempotent = true) - public Parameters andlistWithNoMax( - //@formatter:off - @OperationParam(name="valstr") StringAndListParam theValStr, - @OperationParam(name="valtok") TokenAndListParam theValTok - //@formatter:on + Parameters andlistWithNoMax( + //@formatter:off + @OperationParam(name = "valstr") StringAndListParam theValStr, + @OperationParam(name = "valtok") TokenAndListParam theValTok + //@formatter:on ); @Operation(name = "$nonrepeating", idempotent = true) - public Parameters nonrepeating( - //@formatter:off - @OperationParam(name="valstr") StringParam theValStr, - @OperationParam(name="valtok") TokenParam theValTok - //@formatter:on + Parameters nonrepeating( + //@formatter:off + @OperationParam(name = "valstr") StringParam theValStr, + @OperationParam(name = "valtok") TokenParam theValTok + //@formatter:on ); @Operation(name = "$orlist", idempotent = true) - public Parameters orlist( - //@formatter:off - @OperationParam(name="valstr", max=10) List theValStr, - @OperationParam(name="valtok", max=10) List theValTok - //@formatter:on + Parameters orlist( + //@formatter:off + @OperationParam(name = "valstr", max = 10) List theValStr, + @OperationParam(name = "valtok", max = 10) List theValTok + //@formatter:on ); @Operation(name = "$orlist-withnomax", idempotent = true) - public Parameters orlistWithNoMax( - //@formatter:off - @OperationParam(name="valstr") List theValStr, - @OperationParam(name="valtok") List theValTok - //@formatter:on + Parameters orlistWithNoMax( + //@formatter:off + @OperationParam(name = "valstr") List theValStr, + @OperationParam(name = "valtok") List theValTok + //@formatter:on ); } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } } 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/OperationGenericServerR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationGenericServerR4Test.java index 0f0764c421d..f77882c6f22 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationGenericServerR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationGenericServerR4Test.java @@ -1,10 +1,7 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.FhirContext; -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.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; @@ -31,6 +28,7 @@ import org.junit.BeforeClass; import org.junit.Test; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -168,6 +166,22 @@ public class OperationGenericServerR4Test { } } + + @Test + public void testSearchGetsClassifiedAppropriately() throws Exception { + HttpGet httpPost = new HttpGet("http://localhost:" + ourPort + "/Patient"); + CloseableHttpResponse status = ourClient.execute(httpPost); + try { + assertEquals(200, status.getStatusLine().getStatusCode()); + status.getEntity().getContent().close(); + } finally { + status.getEntity().getContent().close(); + } + + assertEquals("Patient/search", ourLastMethod); + } + + @SuppressWarnings("unused") public static class PatientProvider implements IResourceProvider { @@ -215,6 +229,12 @@ public class OperationGenericServerR4Test { return retVal; } + @Search + public List search() { + ourLastMethod = "Patient/search"; + return new ArrayList<>(); + } + } @@ -239,6 +259,13 @@ public class OperationGenericServerR4Test { } + @Search + public List search() { + ourLastMethod = "/search"; + return new ArrayList<>(); + } + + } @AfterClass 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/interceptor/ServeMediaResourceRawInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptorTest.java
    new file mode 100644
    index 00000000000..469ead376ed
    --- /dev/null
    +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptorTest.java
    @@ -0,0 +1,160 @@
    +package ca.uhn.fhir.rest.server.interceptor;
    +
    +import ca.uhn.fhir.context.FhirContext;
    +import ca.uhn.fhir.rest.annotation.IdParam;
    +import ca.uhn.fhir.rest.annotation.Read;
    +import ca.uhn.fhir.rest.api.Constants;
    +import ca.uhn.fhir.rest.api.EncodingEnum;
    +import ca.uhn.fhir.rest.server.IResourceProvider;
    +import ca.uhn.fhir.rest.server.RestfulServer;
    +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.client.methods.CloseableHttpResponse;
    +import org.apache.http.client.methods.HttpGet;
    +import org.apache.http.impl.client.CloseableHttpClient;
    +import org.apache.http.impl.client.HttpClientBuilder;
    +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.instance.model.api.IIdType;
    +import org.hl7.fhir.r4.model.Media;
    +import org.junit.*;
    +import org.slf4j.Logger;
    +import org.slf4j.LoggerFactory;
    +
    +import java.io.IOException;
    +import java.util.concurrent.TimeUnit;
    +
    +import static org.hamcrest.CoreMatchers.containsString;
    +import static org.junit.Assert.*;
    +
    +public class ServeMediaResourceRawInterceptorTest {
    +
    +
    +	private static final Logger ourLog = LoggerFactory.getLogger(ServeMediaResourceRawInterceptorTest.class);
    +	private static int ourPort;
    +	private static RestfulServer ourServlet;
    +	private static FhirContext ourCtx = FhirContext.forR4();
    +	private static CloseableHttpClient ourClient;
    +	private static Media ourNextResponse;
    +	private static String ourReadUrl;
    +	private ServeMediaResourceRawInterceptor myInterceptor;
    +
    +	@Before
    +	public void before() {
    +		myInterceptor = new ServeMediaResourceRawInterceptor();
    +		ourServlet.registerInterceptor(myInterceptor);
    +	}
    +
    +	@After
    +	public void after() {
    +		ourNextResponse = null;
    +		ourServlet.unregisterInterceptor(myInterceptor);
    +	}
    +
    +	@Test
    +	public void testMediaHasImageRequestHasNoAcceptHeader() throws IOException {
    +		ourNextResponse = new Media();
    +		ourNextResponse.getContent().setContentType("image/png");
    +		ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8});
    +
    +		HttpGet get = new HttpGet(ourReadUrl);
    +		try (CloseableHttpResponse response = ourClient.execute(get)) {
    +			assertEquals("application/fhir+json;charset=utf-8", response.getEntity().getContentType().getValue());
    +			String contents = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
    +			assertThat(contents, containsString("\"resourceType\""));
    +		}
    +	}
    +
    +	@Test
    +	public void testMediaHasImageRequestHasMatchingAcceptHeader() throws IOException {
    +		ourNextResponse = new Media();
    +		ourNextResponse.getContent().setContentType("image/png");
    +		ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8});
    +
    +		HttpGet get = new HttpGet(ourReadUrl);
    +		get.addHeader(Constants.HEADER_ACCEPT, "image/png");
    +		try (CloseableHttpResponse response = ourClient.execute(get)) {
    +			assertEquals("image/png", response.getEntity().getContentType().getValue());
    +			byte[] contents = IOUtils.toByteArray(response.getEntity().getContent());
    +			assertArrayEquals(new byte[]{2, 3, 4, 5, 6, 7, 8}, contents);
    +		}
    +	}
    +
    +	@Test
    +	public void testMediaHasNoContentType() throws IOException {
    +		ourNextResponse = new Media();
    +		ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8});
    +
    +		HttpGet get = new HttpGet(ourReadUrl);
    +		get.addHeader(Constants.HEADER_ACCEPT, "image/png");
    +		try (CloseableHttpResponse response = ourClient.execute(get)) {
    +			assertEquals("application/fhir+json;charset=utf-8", response.getEntity().getContentType().getValue());
    +		}
    +	}
    +
    +	@Test
    +	public void testMediaHasImageRequestHasNonMatchingAcceptHeaderOutputRaw() throws IOException {
    +		ourNextResponse = new Media();
    +		ourNextResponse.getContent().setContentType("image/png");
    +		ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8});
    +
    +		HttpGet get = new HttpGet(ourReadUrl + "?_output=data");
    +		try (CloseableHttpResponse response = ourClient.execute(get)) {
    +			assertEquals("image/png", response.getEntity().getContentType().getValue());
    +			byte[] contents = IOUtils.toByteArray(response.getEntity().getContent());
    +			assertArrayEquals(new byte[]{2, 3, 4, 5, 6, 7, 8}, contents);
    +		}
    +	}
    +
    +	private static class MyMediaResourceProvider implements IResourceProvider {
    +
    +
    +		@Override
    +		public Class getResourceType() {
    +			return Media.class;
    +		}
    +
    +		@Read
    +		public Media read(@IdParam IIdType theId) {
    +			return ourNextResponse;
    +		}
    +
    +	}
    +
    +	@AfterClass
    +	public static void afterClassClearContext() throws IOException {
    +		ourClient.close();
    +		TestUtil.clearAllStaticFieldsForUnitTest();
    +	}
    +
    +	@BeforeClass
    +	public static void beforeClass() throws Exception {
    +		ourPort = PortUtil.findFreePort();
    +
    +		// Create server
    +		ourLog.info("Using port: {}", ourPort);
    +		Server ourServer = new Server(ourPort);
    +		ServletHandler proxyHandler = new ServletHandler();
    +		ourServlet = new RestfulServer(ourCtx);
    +		ourServlet.setDefaultResponseEncoding(EncodingEnum.JSON);
    +		ourServlet.setResourceProviders(new MyMediaResourceProvider());
    +		ServletHolder servletHolder = new ServletHolder(ourServlet);
    +		proxyHandler.addServletWithMapping(servletHolder, "/*");
    +		ourServer.setHandler(proxyHandler);
    +		ourServer.start();
    +
    +		// Create client
    +		PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
    +		HttpClientBuilder builder = HttpClientBuilder.create();
    +		builder.setConnectionManager(connectionManager);
    +		ourClient = builder.build();
    +
    +		ourReadUrl = "http://localhost:" + ourPort + "/Media/123";
    +	}
    +
    +}
    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..121c5a55abd 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;
    @@ -12,6 +13,7 @@ import ca.uhn.fhir.util.TestUtil;
     import org.eclipse.jetty.server.Server;
     import org.eclipse.jetty.servlet.ServletContextHandler;
     import org.eclipse.jetty.servlet.ServletHolder;
    +import org.hl7.fhir.instance.model.api.IAnyResource;
     import org.hl7.fhir.instance.model.api.IIdType;
     import org.hl7.fhir.r4.model.Bundle;
     import org.hl7.fhir.r4.model.Observation;
    @@ -89,7 +91,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());
     
    @@ -229,7 +238,7 @@ public class HashMapResourceProviderTest {
     		Bundle resp = ourClient
     			.search()
     			.forResource("Patient")
    -			.where(Patient.RES_ID.exactly().codes("2", "3"))
    +			.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
     			.returnBundle(Bundle.class).execute();
     		assertEquals(2, resp.getTotal());
     		assertEquals(2, resp.getEntry().size());
    @@ -240,8 +249,8 @@ public class HashMapResourceProviderTest {
     		resp = ourClient
     			.search()
     			.forResource("Patient")
    -			.where(Patient.RES_ID.exactly().codes("2", "3"))
    -			.where(Patient.RES_ID.exactly().codes("2", "3"))
    +			.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
    +			.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
     			.returnBundle(Bundle.class).execute();
     		assertEquals(2, resp.getTotal());
     		assertEquals(2, resp.getEntry().size());
    @@ -251,8 +260,8 @@ public class HashMapResourceProviderTest {
     		resp = ourClient
     			.search()
     			.forResource("Patient")
    -			.where(Patient.RES_ID.exactly().codes("2", "3"))
    -			.where(Patient.RES_ID.exactly().codes("4", "3"))
    +			.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
    +			.where(IAnyResource.RES_ID.exactly().codes("4", "3"))
     			.returnBundle(Bundle.class).execute();
     		respIds = resp.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList());
     		assertThat(respIds, containsInAnyOrder("Patient/3"));
    diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml
    index a006ebcb355..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-SNAPSHOT
    +		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-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirServerConfig.java b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirServerConfig.java
    index 16d8a4c29eb..e0a78fd7b41 100644
    --- a/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirServerConfig.java
    +++ b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirServerConfig.java
    @@ -26,7 +26,7 @@ import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
     @EnableTransactionManagement()
     public class FhirServerConfig {
     
    -	@Bean()
    +	@Bean
     	public DaoConfig daoConfig() {
     		DaoConfig retVal = new DaoConfig();
     		return retVal;
    diff --git a/hapi-fhir-tutorial/jpaserver-example-with-custom/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml b/hapi-fhir-tutorial/jpaserver-example-with-custom/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml
    index f64b5b5dbe6..3c750b12e76 100644
    --- a/hapi-fhir-tutorial/jpaserver-example-with-custom/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml
    +++ b/hapi-fhir-tutorial/jpaserver-example-with-custom/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml
    @@ -41,6 +41,7 @@
     		
     			
     				ca.uhn.fhir.jpa.entity
    +				ca.uhn.fhir.jpa.model.entity
     				ca.uhn.fhir.jpa.demo.entity
     			
     		
    diff --git a/hapi-fhir-tutorial/jpaserver-example-with-custom/src/test/resources/test-hapi-fhir-server-database-config.xml b/hapi-fhir-tutorial/jpaserver-example-with-custom/src/test/resources/test-hapi-fhir-server-database-config.xml
    index 7f2d4baee76..2b5d014caad 100644
    --- a/hapi-fhir-tutorial/jpaserver-example-with-custom/src/test/resources/test-hapi-fhir-server-database-config.xml
    +++ b/hapi-fhir-tutorial/jpaserver-example-with-custom/src/test/resources/test-hapi-fhir-server-database-config.xml
    @@ -41,6 +41,7 @@
     		
     			
     				ca.uhn.fhir.jpa.entity
    +				ca.uhn.fhir.jpa.model.entity
     				ca.uhn.fhir.jpa.demo.entity
     			
     		
    diff --git a/hapi-fhir-utilities/pom.xml b/hapi-fhir-utilities/pom.xml
    index 9ce3490b479..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-SNAPSHOT
    +		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 80e4feea5a8..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-SNAPSHOT
    +		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 6656ea04a7c..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-SNAPSHOT
    +		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 7f63d014d3c..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-SNAPSHOT
    +		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 827053fc7c6..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-SNAPSHOT
    +		3.7.0-SNAPSHOT
     		../hapi-deployable-pom/pom.xml
     	
     
    diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml
    index 24631137329..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-SNAPSHOT
    +		3.7.0-SNAPSHOT
     		../hapi-deployable-pom/pom.xml
     	
     
    diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml
    index 531eec78429..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-SNAPSHOT
    +		3.7.0-SNAPSHOT
     		../pom.xml
     	
     
    @@ -73,7 +73,7 @@
     		
     			ca.uhn.hapi.fhir
     			hapi-fhir-structures-r4
    -			3.6.0-SNAPSHOT
    +			3.7.0-SNAPSHOT
     		
     		
     			ca.uhn.hapi.fhir
    diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm
    index ac572a98850..e2c3d23def3 100644
    --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm
    +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm
    @@ -6,7 +6,7 @@ import java.util.*;
     import org.apache.commons.lang3.StringUtils;
     
     import ca.uhn.fhir.jpa.provider${package_suffix}.*;
    -import ca.uhn.fhir.jpa.dao.SearchParameterMap;
    +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
     import ca.uhn.fhir.model.api.Include;
     import ca.uhn.fhir.model.api.annotation.*;
     #if ( $isRi )
    diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml
    index 6e8750cec40..ebc47f7eaf3 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-SNAPSHOT
    +		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 304f6533d96..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-SNAPSHOT
    +		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 c177c688846..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-SNAPSHOT
    +		3.7.0-SNAPSHOT
     		../../hapi-deployable-pom/pom.xml
     	
     
    diff --git a/pom.xml b/pom.xml
    index 50cc673a3af..39f7bc79476 100644
    --- a/pom.xml
    +++ b/pom.xml
    @@ -6,7 +6,7 @@
     	ca.uhn.hapi.fhir
     	hapi-fhir
     	pom
    -	3.6.0-SNAPSHOT
    +	3.7.0-SNAPSHOT
     	HAPI-FHIR
     	An open-source implementation of the FHIR specification in Java.
     	https://hapifhir.io
    @@ -433,6 +433,7 @@
     		
     		
     			RuthAlk
    +			Ruth Alkema
     		
     		
     			Tastelezz
    @@ -473,6 +474,14 @@
     		
     			jbalbien
     		
    +		
    +			volsch
    +			Volker Schmidt
    +		
    +		
    +			magnuswatn
    +			Magnus Watn
    +		
     	
     
     	
    @@ -513,11 +522,12 @@
     		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
     		5.5.5
     		2.5.3
     		1.8
    @@ -530,8 +540,8 @@
     		1.7.25
     		5.0.8.RELEASE
     		2.0.7.RELEASE
    -
     		1.5.6.RELEASE
    +
     		3.1.4
     		3.0.9.RELEASE
     		4.4.1
    @@ -563,32 +573,32 @@
     			
     				com.fasterxml.jackson.core
     				jackson-annotations
    -				2.9.2
    +				${jackson_version}
     			
     			
     				com.fasterxml.jackson.core
     				jackson-core
    -				2.9.2
    +				${jackson_version}
     			
     			
     				com.fasterxml.jackson.core
     				jackson-databind
    -				2.9.2
    +				${jackson_version}
     			
     			
     				com.fasterxml.jackson.datatype
     				jackson-datatype-jsr310
    -				2.9.2
    +				${jackson_version}
     			
     			
     				com.fasterxml.jackson.dataformat
     				jackson-dataformat-yaml
    -				2.9.2
    +				${jackson_version}
     			
     			
     				com.fasterxml.jackson.module
     				jackson-module-jaxb-annotations
    -				2.9.2
    +				${jackson_version}
     			
     			
     				com.github.ben-manes.caffeine
    @@ -673,7 +683,7 @@
     			
     				org.apache.commons
     				commons-compress
    -				1.14
    +				1.18
     			
     			
     				commons-io
    @@ -1240,7 +1250,7 @@
     			
     			
     				org.thymeleaf
    -				thymeleaf-spring4
    +				thymeleaf-spring5
     				${thymeleaf-version}
     			
     			
    @@ -1270,6 +1280,14 @@
     				true
     			
     		
    +		
    +			maven2
    +			Maven2
    +			http://central.maven.org/maven2/
    +			
    +				true
    +			
    +		
     	
     
     	
    @@ -1305,17 +1323,9 @@
     					1.2.1
     				
     				
    -					de.juplo
    -					hibernate-maven-plugin
    -					2.1.1
    -					
    -						false
    -						false
    -						false
    -						false
    -						true
    -						true
    -					
    +					de.jpdigital
    +					hibernate52-ddl-maven-plugin
    +					2.2.0
     				
     				
     					org.apache.felix
    @@ -1727,6 +1737,12 @@
     								
     									
     								
    +								
    +									
    +								
    +								
    +									
    +								
     								
     									
     								
    @@ -2057,22 +2073,23 @@
     					
     				
     			
    -			
    +			
     			
     		
     	
    @@ -2131,6 +2148,11 @@
     				hapi-fhir-structures-dstu2
     				hapi-fhir-structures-dstu3
     				hapi-fhir-structures-r4
    +				hapi-fhir-client
    +				hapi-fhir-server
    +				hapi-fhir-jpaserver-model
    +				hapi-fhir-jpaserver-searchparam
    +				hapi-fhir-jpaserver-subscription
     				hapi-fhir-jpaserver-base
     				hapi-fhir-jaxrsserver-base
     				
    @@ -2181,6 +2203,9 @@
     				hapi-fhir-structures-r4
     				hapi-fhir-validation-resources-r4
     				hapi-fhir-igpacks
    +				hapi-fhir-jpaserver-model
    +				hapi-fhir-jpaserver-searchparam
    +				hapi-fhir-jpaserver-subscription
     				hapi-fhir-jaxrsserver-base
     				hapi-fhir-jaxrsserver-example
     				hapi-fhir-jpaserver-base
    diff --git a/restful-server-example-test/pom.xml b/restful-server-example-test/pom.xml
    index 6ba0dd78b32..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-SNAPSHOT
    +		3.7.0-SNAPSHOT
     		../pom.xml
     	
     
    diff --git a/restful-server-example/pom.xml b/restful-server-example/pom.xml
    index 357246d581d..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-SNAPSHOT
    +		3.7.0-SNAPSHOT
     		../pom.xml
     	
     	
    diff --git a/src/changes/changes.xml b/src/changes/changes.xml
    index c1562231fa3..db2d2cf4157 100644
    --- a/src/changes/changes.xml
    +++ b/src/changes/changes.xml
    @@ -6,13 +6,152 @@
     		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. + + + In the JPA server, when performing a chained reference search on a search parameter with + a target type of + Reference(Any)]]>, the search failed with an incomprehensible + error. This has been corrected to return an error message indicating that the chain + must be qualified with a resource type for such a field. For example, + QuestionnaireResponse?subject:Patient.name=smith]]> + instead of + QuestionnaireResponse?subject.name=smith]]>. + + + The LOINC uploader has been updated to suport the LOINC 2.65 release + file format. + + + The resource reindexer can now detect when a resource's current version no longer + exists in the database (e.g. because it was manually expunged), and can automatically + adjust the most recent version to + account for this. + + + When updating existing resources, the JPA server will now attempt to reuse/update + rows in the index tables if one row is being removed and one row is being added (e.g. + because a Patient's name is changing from "A" to "B"). This has the net effect + of reducing the number + + + An issue was corrected with the JPA reindexer, where String index columns do not always + get reindexed if they did not have an identity hash value in the HASH_IDENTITY column. + + + Plain Server ResourceProvider classes are no longer required to be public classes. This + limitation has always been enforced, but did not actually serve any real purpose so it + has been removed. + + + A new interceptor called ServeMediaResourceRawInterceptor has been added. This interceptor + causes Media resources to be served as raw content if the client explicitly requests + the correct content type cia the Accept header. + +
    + The version of a few dependencies have been bumped to the latest versions (dependent HAPI modules listed in brackets):
  • Karaf (OSGi): 4.1.4 -> 4.1.6
  • +
  • Commons-Compress (JPA): 1.14 -> 1.18
  • +
  • Jackson (JPA): 2.9.2 -> 2.9.7
  • ]]>
    @@ -27,14 +166,15 @@ has been streamlined to generate more predictable IDs in some cases. - An issue in the HAPI FHIR CLI database mogrator command has been resolved, where + An issue in the HAPI FHIR CLI database migrator command has been resolved, where some database drivers did not automatically register and had to be manually added to the classpath. 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 running. + to avoid having very long running delete operations occupying database connections for a + long time or timing out. When invoking an operation using the fluent client on an instance, the operation would @@ -45,6 +185,11 @@ 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 + this link]]> + for a description of how this feature works. Note that you must add the + SubscriptionRetriggeringProvider as shown in the sample project + here.]]> When operating in R4 mode, the HAPI FHIR server will now populate Bundle.entry.response @@ -100,11 +245,6 @@ permission is granted. This has been corrected so that transaction() allows both batch and transaction requests to proceed. - - The AuthorizationInterceptor was previously not able to authorize the FHIR - batch operation. As of this version, when authorizing a transaction operation - (via the transaction() rule), both batch and transaction will be allowed. - The JPA server now automatically supplies several appropriate hibernate performance settings as long as the JPA EntityManagerFactory was created using HAPI FHIR's @@ -125,6 +265,7 @@ The FhirTerser getValues(...)]]> methods were not properly handling modifier extensions for verions of FHIR prior to DSTU3. This has been corrected. + When updating resources in the JPA server, a bug caused index table entries to be refreshed sometimes even though the index value hadn't changed. This issue did not cause incorrect search @@ -142,6 +283,48 @@ to use a parameter annotated with @ResourceParam to receive the Parameters (or other) resource supplied by the client as the request body. + + The JPA server version migrator tool now runs in a multithreaded way, allowing it to + upgrade th database faster when migration tasks require data updates. + + + A bug in the JPA server was fixed: When a resource was previously deleted, + a transaction could not be posted that both restored the deleted resource but + also contained references to the now-restored resource. + + + The $expunge operation could sometimes fail to delete resources if a resource + to be deleted had recently been returned in a search result. This has been + corrected. + + + A new setting has been added to the JPA Server DopConfig that controls the + behaviour when a client-assigned ID is encountered (i.e. the client performs + an HTTP PUT to a resource ID that doesn't already exist on the server). It is + now possible to disallow this action, to only allow alphanumeric IDs (the default + and only option previously) or allow any IDs including alphanumeric. + + + It is now possible to use your own IMessageResolver instance in the narrative + generator. Thanks to Ruth Alkema for the pull request! + + + When restful reponses tried to return multiple instances of the same response header, + some instances were discarded. Thanks to Volker Schmidt for the pull request! + + + The REST client now allows for configurable behaviour as to whether a + _format]]> + parameter should be included in requests. + + + JPA server R4 SearchParameter custom expression validation is now done using the + actual FHIRPath evaluator, meaning it is more rigorous in what it can find. + + + A NullPointerException in DateRangeParam when a client URL conrtained a malformed + date was corrected. Thanks Heinz-Dieter Conradi for the Pull Request! +
    diff --git a/src/site/site.xml b/src/site/site.xml index 2c5ca40def0..d0e75165cc7 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -115,6 +115,8 @@ + + diff --git a/src/site/xdoc/doc_cli.xml b/src/site/xdoc/doc_cli.xml index 8f65b8732d8..5598f02a7fa 100644 --- a/src/site/xdoc/doc_cli.xml +++ b/src/site/xdoc/doc_cli.xml @@ -142,42 +142,16 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)]]>

    - When upgrading the JPA server from one version of HAPI FHIR to a newer version, - often there will be changes to the database schema. The Migrate Database - command can be used to perform a migration from one version to the next. -

    -

    - Note that this feature was added in HAPI FHIR 3.5.0. It is not able to migrate - from versions prior to HAPI FHIR 3.4.0. Please make a backup of your - database before running this command! -

    -

    - The following example shows how to use the migrator utility to migrate between two versions. -

    -
    ./hapi-fhir-cli migrate-database -d DERBY_EMBEDDED -u "jdbc:derby:directory:target/jpaserver_derby_files;create=true" -n "" -p "" -f V3_4_0 -t V3_5_0
    - -

    - You may use the following command to get detailed help on the options: -

    -
    ./hapi-fhir-cli help migrate-database
    - -

    - Note the arguments: -

      -
    • -d [dialect] - This indicates the database dialect to use. See the detailed help for a list of options
    • -
    • -f [version] - The version to migrate from
    • -
    • -t [version] - The version to migrate to
    • -
    + The migrate-database command may be used to Migrate a database + schema when upgrading a + HAPI FHIR JPA project from one version of HAPI + FHIR to another version.

    - -

    - Note that the Oracle JDBC drivers are not distributed in the Maven Central repository, - so they are not included in HAPI FHIR. In order to use this command with an Oracle database, - you will need to invoke the CLI as follows: -

    -
    java -cp hapi-fhir-cli.jar ca.uhn.fhir.cli.App migrate-database -d ORACLE_12C -u "[url]" -n "[username]" -p "[password]" -f V3_4_0 -t V3_5_0
    -
    +

    + See Upgrading HAPI FHIR JPA + for information on how to use this command. +

    diff --git a/src/site/xdoc/doc_jpa.xml b/src/site/xdoc/doc_jpa.xml index a79c2d83689..3945b498729 100644 --- a/src/site/xdoc/doc_jpa.xml +++ b/src/site/xdoc/doc_jpa.xml @@ -101,7 +101,7 @@ $ mvn install]]> The Spring confguration contains a definition for a bean called daoConfig, which will look something like the following:

    - - - - + + +
    + +

    + HAPI FHIR JPA is a constantly evolving product, with new features being added to each + new version of the library. As a result, it is generally necessary to execute a database + migration as a part of an upgrade to HAPI FHIR. +

    + +

    + When upgrading the JPA server from one version of HAPI FHIR to a newer version, + often there will be changes to the database schema. The Migrate Database + command can be used to perform a migration from one version to the next. +

    + +

    + Note that this feature was added in HAPI FHIR 3.5.0. It is not able to migrate + from versions prior to HAPI FHIR 3.4.0. Please make a backup of your + database before running this command! +

    +

    + The following example shows how to use the migrator utility to migrate between two versions. +

    +
    ./hapi-fhir-cli migrate-database -d DERBY_EMBEDDED -u "jdbc:derby:directory:target/jpaserver_derby_files;create=true" -n "" -p "" -f V3_4_0 -t V3_5_0
    + +

    + You may use the following command to get detailed help on the options: +

    +
    ./hapi-fhir-cli help migrate-database
    + +

    + Note the arguments: +

      +
    • -d [dialect] - This indicates the database dialect to use. See the detailed help for a list of options
    • +
    • -f [version] - The version to migrate from
    • +
    • -t [version] - The version to migrate to
    • +
    +

    + + +

    + Note that the Oracle JDBC drivers are not distributed in the Maven Central repository, + so they are not included in HAPI FHIR. In order to use this command with an Oracle database, + you will need to invoke the CLI as follows: +

    +
    java -cp hapi-fhir-cli.jar ca.uhn.fhir.cli.App migrate-database -d ORACLE_12C -u "[url]" -n "[username]" -p "[password]" -f V3_4_0 -t V3_5_0
    +
    + + +

    + As of HAPI FHIR 3.5.0 a new mechanism for creating the JPA index tables (HFJ_SPIDX_xxx) + has been implemented. This new mechanism uses hashes in place of large multi-column + indexes. This improves both lookup times as well as required storage space. This change + also paves the way for future ability to provide efficient multi-tenant searches (which + is not yet implemented but is planned as an incremental improvement). +

    +

    + This change is not a lightweight change however, as it requires a rebuild of the + index tables in order to generate the hashes. This can take a long time on databases + that already have a large amount of data. +

    +

    + As a result, in HAPI FHIR JPA 3.6.0, an efficient way of upgrading existing databases + was added. Under this new scheme, columns for the hashes are added but values are not + calculated initially, database indexes are not modified on the HFJ_SPIDX_xxx tables, + and the previous columns are still used for searching as was the case in HAPI FHIR + JPA 3.4.0. +

    +

    + In order to perform a migration using this functionality, the following steps should + be followed: +

    +
      +
    • + Stop your running HAPI FHIR JPA instance (and remember to make a backup of your + database before proceeding with any changes!) +
    • +
    • + Modify your DaoConfig to specify that hash-based searches should not be used, using + the following setting:
      +
      myDaoConfig.setDisableHashBasedSearches(true);
      +
    • +
    • + Make sure that you have your JPA settings configured to not automatically + create database indexes and columns using the following setting + in your JPA Properties:
      +
      extraProperties.put("hibernate.hbm2ddl.auto", "none");
      +
    • +
    • + Run the database migrator command, including the entry -x no-migrate-350-hashes + on the command line. For example:
      +
      ./hapi-fhir-cli migrate-database -d DERBY_EMBEDDED -u "jdbc:derby:directory:target/jpaserver_derby_files;create=true" -n "" -p "" -f V3_4_0 -t V3_6_0 -x no-migrate-350-hashes
      +
    • +
    • + Rebuild and start your HAPI FHIR JPA server. At this point you should have a working + HAPI FHIR JPA 3.6.0 server that is is still using HAPI FHIR 3.4.0 search indexes. Search hashes + will be generated for any newly created or updated data but existing data will have null + hashes. +
    • +
    • + With the system running, request a complete reindex of the data in the database using + an HTTP request such as the following:
      +
      GET /$mark-all-resources-for-reindexing
      + Note that this is a custom operation built into the HAPI FHIR JPA server. It should + be secured in a real deployment, so Authentication is likely required for this + call. +
    • +
    • + You can track the reindexing process by watching your server logs, + but also by using the following SQL executed directly against your database: +
      +
      SELECT * FROM HFJ_RES_REINDEX_JOB
      + When this query no longer returns any rows, the reindexing process is complete. +
    • +
    • + At this time, HAPI FHIR should be stopped once again in order to convert it + to using the hash based indexes. +
    • +
    • + Modify your DaoConfig to specify that hash-based searches are used, using + the following setting (this is the default setting, so it could also simply + be omitted):
      +
      myDaoConfig.setDisableHashBasedSearches(false);
      +
    • +
    • + Execute the migrator tool again, this time omitting the flag option, e.g.
      +
      ./hapi-fhir-cli migrate-database -d DERBY_EMBEDDED -u "jdbc:derby:directory:target/jpaserver_derby_files;create=true" -n "" -p "" -f V3_4_0 -t V3_6_0
      +
    • +
    • + Rebuild, and start HAPI FHIR JPA again. +
    • +
    +
    + +
    + diff --git a/src/site/xdoc/docindex.xml b/src/site/xdoc/docindex.xml index 49cefb6481c..226d7e8b457 100644 --- a/src/site/xdoc/docindex.xml +++ b/src/site/xdoc/docindex.xml @@ -1,86 +1,88 @@ - - - - - Documentation - James Agnew - - - - -
    - -

    - Welcome to HAPI FHIR! We hope that the documentation here will be - helpful to you. -

    - -
    - -

    The Data Model

    - - -

    RESTful Client

    - - -

    RESTful Server

    - - -

    Other Features

    - - - -

    JavaDocs

    - - -

    Source Cross Reference

    - - -
    - - - - + + + + + Documentation + James Agnew + + + + +
    + +

    + Welcome to HAPI FHIR! We hope that the documentation here will be + helpful to you. +

    + + + +

    The Data Model

    + + +

    RESTful Client

    + + +

    RESTful Server

    + + +

    Other Features

    + + + +

    JavaDocs

    + + +

    Source Cross Reference

    + + +
    + + + +
    diff --git a/src/site/xdoc/download.xml.vm b/src/site/xdoc/download.xml.vm index ed9fb7f20e4..4b8a99422a3 100644 --- a/src/site/xdoc/download.xml.vm +++ b/src/site/xdoc/download.xml.vm @@ -194,7 +194,25 @@ 3.0.1 3.4.0-13732 - + + HAPI FHIR 3.5.0 + JDK8 + + 1.0.2 + 1.4.0 + 3.0.1 + 3.4.0-13732 + + + HAPI FHIR 3.6.0-SNAPSHOT + JDK8 + + 1.0.2 + 1.4.0 + 3.0.1 + 3.6.0-1202b2eed0f + + diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml index 4585434f8dc..f6257ec2684 100644 --- a/src/site/xdoc/index.xml +++ b/src/site/xdoc/index.xml @@ -67,6 +67,66 @@
    +

    + Nov 12, 2018 - HAPI FHIR 3.6.0 (Food) Released - + The next release of HAPI has now been uploaded to the Maven repos and + GitHub's releases section. +

    +

    + This release brings us back to our regular 3 month release cycle (although we're + only two months after the last release, which was delayed more than we were hoping). + This also marks the beginning of codenamed major releases. Our first codename + is Food, and we will be following the popular (and admittedly unoriginal) strategy + of using the next letter in the alphabet for each release. +

    +

    + As always, see the + changelog for a full list + of changes. Notable changes include: +

    + +
      +
    • + The FHIR R4 structures have been upgraded to the latest (3.6.0) version of the structures. + This marks an exciting (but pointless) milestone that HAPI FHIR and FHIR itself have the + same version number! +
    • +
    • + The JPA Server migrator tool has been enhanced so that it is now possible + to run a rolling migration from 3.4.0 to 3.6.0 instead of needing to incur + a long downtime while the indexes are rebuilt. See this link for details. In addition, the migrator can now migrate + HAPI FHIR 3.3.0 as well. This tool now also operates in a multithreaded way, + meaning that it can run migrations much faster in systems with a lot of data. +
    • +
    • + A new custom FHIR operation has been added, allowing subscriptions to be manually + triggered/retriggered. This means that it is possible to cause a subscription + to process a resource in the database as though that resource had been updated, + without actually updating it. +
    • +
    • + The JPA SearchCoordinator now pre-fetches only the first few pages of a search + by default instead of pre-fetching all possible results. This makes searches + dramatically more efficient in servers where users commonly perform searches + that could potentially return many pages but only actually load the first few. +
    • +
    • + A new JPA sample project has been added. This sample has existed for a while, but + this is now the offical "reference" project for anyone looking to get started with + HAPI FHIR JPA. +
    • +
    +

    + Thanks to everyone who contributed to this release! +

    +

    + - James Agnew +

    +

    + + + +

    Sep 17, 2018 - HAPI FHIR 3.5.0 Released - The next release of HAPI has now been uploaded to the Maven repos and @@ -95,7 +155,7 @@ as they come out.

  • - A new databasse migration tool has been added to the HAPI FHIR CLI. This tool + A new database migration tool has been added to the HAPI FHIR CLI. This tool allows administrators to automatically upgrade an existing database schema from a previous version of HAPI FHIR (currently only HAPI FHIR 3.4.0 is supported) to the latest version. See the @@ -137,6 +197,7 @@



    +