diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..3829daa7291 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +NOTE: Before filing a ticket, please see the following URL: +https://github.com/jamesagnew/hapi-fhir/wiki/Getting-Help + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment (please complete the following information):** + - HAPI FHIR Version + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + +**Additional context** +Add any other context about the problem here. diff --git a/README.md b/README.md index ad66edaa9d7..d753df33492 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,5 @@ A demonstration of this project is available here: http://hapi.fhir.org/ This project is Open Source, licensed under the Apache Software License 2.0. + +Please see [this wiki page](https://github.com/jamesagnew/hapi-fhir/wiki/Getting-Help) for information on where to get help with HAPI FHIR. Please see [Smile CDR](https://smilecdr.com) for information on commercial support. diff --git a/examples/src/main/java/example/AuthorizationInterceptors.java b/examples/src/main/java/example/AuthorizationInterceptors.java index 71132a821f2..3ef5822b6d1 100644 --- a/examples/src/main/java/example/AuthorizationInterceptors.java +++ b/examples/src/main/java/example/AuthorizationInterceptors.java @@ -4,6 +4,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.List; +import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.model.dstu2.resource.Patient; @@ -140,5 +141,21 @@ public class AuthorizationInterceptors { } }; //END SNIPPET: authorizeTenantAction + + + //START SNIPPET: patchAll + new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + // Authorize patch requests + .allow().patch().allRequests().andThen() + // Authorize actual writes that patch may perform + .allow().write().allResources().inCompartment("Patient", new IdType("Patient/123")).andThen() + .build(); + } + }; + //END SNIPPET: patchAll + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index 01ccc3ca7b8..86514b5ae53 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -1,7 +1,34 @@ package ca.uhn.fhir.context; +import ca.uhn.fhir.context.api.AddProfileTagEnum; +import ca.uhn.fhir.context.support.IContextValidationSupport; +import ca.uhn.fhir.fluentpath.IFluentPath; +import ca.uhn.fhir.i18n.HapiLocalizer; +import ca.uhn.fhir.model.api.IElement; +import ca.uhn.fhir.model.api.IFhirVersion; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.view.ViewGenerator; +import ca.uhn.fhir.narrative.INarrativeGenerator; +import ca.uhn.fhir.parser.*; +import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory; +import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.IRestfulClient; +import ca.uhn.fhir.rest.client.api.IRestfulClientFactory; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ReflectionUtil; +import ca.uhn.fhir.util.VersionUtil; +import ca.uhn.fhir.validation.FhirValidator; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; + import java.io.IOException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.Map.Entry; /* * #%L @@ -23,30 +50,10 @@ import java.lang.reflect.Method; * #L% */ -import java.lang.reflect.Modifier; -import java.util.*; -import java.util.Map.Entry; - -import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.*; - -import ca.uhn.fhir.context.api.AddProfileTagEnum; -import ca.uhn.fhir.context.support.IContextValidationSupport; -import ca.uhn.fhir.fluentpath.IFluentPath; -import ca.uhn.fhir.i18n.HapiLocalizer; -import ca.uhn.fhir.model.api.*; -import ca.uhn.fhir.model.view.ViewGenerator; -import ca.uhn.fhir.narrative.INarrativeGenerator; -import ca.uhn.fhir.parser.*; -import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory; -import ca.uhn.fhir.rest.client.api.*; -import ca.uhn.fhir.util.*; -import ca.uhn.fhir.validation.FhirValidator; - /** * The FHIR context is the central starting point for the use of the HAPI FHIR API. It should be created once, and then * used as a factory for various other types of objects (parsers, clients, etc.). - * + * *

* Important usage notes: *

@@ -68,6 +75,7 @@ public class FhirContext { private static final List> EMPTY_LIST = Collections.emptyList(); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirContext.class); + private final IFhirVersion myVersion; private AddProfileTagEnum myAddProfileTagWhenEncoding = AddProfileTagEnum.ONLY_FOR_CUSTOM; private volatile Map, BaseRuntimeElementDefinition> myClassToElementDefinition = Collections.emptyMap(); private ArrayList> myCustomTypes; @@ -87,14 +95,11 @@ public class FhirContext { private volatile IRestfulClientFactory myRestfulClientFactory; private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition; private IContextValidationSupport myValidationSupport; - - private final IFhirVersion myVersion; - private Map>> myVersionToNameToResourceType = Collections.emptyMap(); /** * @deprecated It is recommended that you use one of the static initializer methods instead - * of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()} or {@link #forR4()} + * of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()} or {@link #forR4()} */ @Deprecated public FhirContext() { @@ -103,7 +108,7 @@ public class FhirContext { /** * @deprecated It is recommended that you use one of the static initializer methods instead - * of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()} or {@link #forR4()} + * of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()} or {@link #forR4()} */ @Deprecated public FhirContext(Class theResourceType) { @@ -112,7 +117,7 @@ public class FhirContext { /** * @deprecated It is recommended that you use one of the static initializer methods instead - * of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()} or {@link #forR4()} + * of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()} or {@link #forR4()} */ @Deprecated public FhirContext(Class... theResourceTypes) { @@ -121,7 +126,7 @@ public class FhirContext { /** * @deprecated It is recommended that you use one of the static initializer methods instead - * of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()} or {@link #forR4()} + * of this method, e.g. {@link #forDstu2()} or {@link #forDstu3()} or {@link #forR4()} */ @Deprecated public FhirContext(Collection> theResourceTypes) { @@ -161,7 +166,7 @@ public class FhirContext { if (theVersion == null) { ourLog.info("Creating new FhirContext with auto-detected version [{}]. It is recommended to explicitly select a version for future compatibility by invoking FhirContext.forDstuX()", - myVersion.getVersion().name()); + myVersion.getVersion().name()); } else { ourLog.info("Creating new FHIR context for FHIR version [{}]", myVersion.getVersion().name()); } @@ -201,13 +206,37 @@ public class FhirContext { * When encoding resources, this setting configures the parser to include * an entry in the resource's metadata section which indicates which profile(s) the * resource claims to conform to. The default is {@link AddProfileTagEnum#ONLY_FOR_CUSTOM}. - * + * * @see #setAddProfileTagWhenEncoding(AddProfileTagEnum) for more information */ public AddProfileTagEnum getAddProfileTagWhenEncoding() { return myAddProfileTagWhenEncoding; } + /** + * When encoding resources, this setting configures the parser to include + * an entry in the resource's metadata section which indicates which profile(s) the + * resource claims to conform to. The default is {@link AddProfileTagEnum#ONLY_FOR_CUSTOM}. + *

+ * This feature is intended for situations where custom resource types are being used, + * avoiding the need to manually add profile declarations for these custom types. + *

+ *

+ * See Profiling and Extensions + * for more information on using custom types. + *

+ *

+ * Note that this feature automatically adds the profile, but leaves any profile tags + * which have been manually added in place as well. + *

+ * + * @param theAddProfileTagWhenEncoding The add profile mode (must not be null) + */ + public void setAddProfileTagWhenEncoding(AddProfileTagEnum theAddProfileTagWhenEncoding) { + Validate.notNull(theAddProfileTagWhenEncoding, "theAddProfileTagWhenEncoding must not be null"); + myAddProfileTagWhenEncoding = theAddProfileTagWhenEncoding; + } + Collection getAllResourceDefinitions() { validateInitialized(); return myNameToResourceDefinition.values(); @@ -215,7 +244,7 @@ public class FhirContext { /** * Returns the default resource type for the given profile - * + * * @see #setDefaultTypeForProfile(String, Class) */ public Class getDefaultTypeForProfile(String theProfile) { @@ -249,7 +278,9 @@ public class FhirContext { return myNameToElementDefinition.get(theElementName.toLowerCase()); } - /** For unit tests only */ + /** + * For unit tests only + */ int getElementDefinitionCount() { validateInitialized(); return myClassToElementDefinition.size(); @@ -274,20 +305,43 @@ public class FhirContext { return myLocalizer; } + /** + * This feature is not yet in its final state and should be considered an internal part of HAPI for now - use with + * caution + */ + public void setLocalizer(HapiLocalizer theMessages) { + myLocalizer = theMessages; + } + public INarrativeGenerator getNarrativeGenerator() { return myNarrativeGenerator; } + public void setNarrativeGenerator(INarrativeGenerator theNarrativeGenerator) { + myNarrativeGenerator = theNarrativeGenerator; + } + /** * Returns the parser options object which will be used to supply default * options to newly created parsers - * + * * @return The parser options - Will not return null */ public ParserOptions getParserOptions() { return myParserOptions; } + /** + * Sets the parser options object which will be used to supply default + * options to newly created parsers + * + * @param theParserOptions The parser options object - Must not be null + */ + public void setParserOptions(ParserOptions theParserOptions) { + Validate.notNull(theParserOptions, "theParserOptions must not be null"); + myParserOptions = theParserOptions; + } + /** * Get the configured performance options */ @@ -295,6 +349,32 @@ public class FhirContext { return myPerformanceOptions; } + // /** + // * Return an unmodifiable collection containing all known resource definitions + // */ + // public Collection getResourceDefinitions() { + // + // Set> datatypes = Collections.emptySet(); + // Map, BaseRuntimeElementDefinition> existing = Collections.emptyMap(); + // HashMap> types = new HashMap>(); + // ModelScanner.scanVersionPropertyFile(datatypes, types, myVersion.getVersion(), existing); + // for (int next : types.) + // + // return Collections.unmodifiableCollection(myIdToResourceDefinition.values()); + // } + + /** + * Sets the configured performance options + * + * @see PerformanceOptionsEnum for a list of available options + */ + public void setPerformanceOptions(Collection theOptions) { + myPerformanceOptions.clear(); + if (theOptions != null) { + myPerformanceOptions.addAll(theOptions); + } + } + /** * Returns the scanned runtime model for the given type. This is an advanced feature which is generally only needed * for extending the core library. @@ -359,8 +439,12 @@ public class FhirContext { *

* Note that this method is case insensitive! *

+ * + * @throws DataFormatException If the resource name is not known */ - public RuntimeResourceDefinition getResourceDefinition(String theResourceName) { + // Multiple spots in HAPI FHIR and Smile CDR depend on DataFormatException being + // thrown by this method, don't change that. + public RuntimeResourceDefinition getResourceDefinition(String theResourceName) throws DataFormatException { validateInitialized(); Validate.notBlank(theResourceName, "theResourceName must not be blank"); @@ -380,20 +464,6 @@ public class FhirContext { return retVal; } - // /** - // * Return an unmodifiable collection containing all known resource definitions - // */ - // public Collection getResourceDefinitions() { - // - // Set> datatypes = Collections.emptySet(); - // Map, BaseRuntimeElementDefinition> existing = Collections.emptyMap(); - // HashMap> types = new HashMap>(); - // ModelScanner.scanVersionPropertyFile(datatypes, types, myVersion.getVersion(), existing); - // for (int next : types.) - // - // return Collections.unmodifiableCollection(myIdToResourceDefinition.values()); - // } - /** * Returns the scanned runtime model for the given type. This is an advanced feature which is generally only needed * for extending the core library. @@ -412,10 +482,40 @@ public class FhirContext { return myIdToResourceDefinition.values(); } + /** + * Returns an unmodifiable set containing all resource names known to this + * context + */ + public Set getResourceNames() { + Set resourceNames = new HashSet<>(); + + if (myNameToResourceDefinition.isEmpty()) { + Properties props = new Properties(); + try { + props.load(myVersion.getFhirVersionPropertiesFile()); + } catch (IOException theE) { + throw new ConfigurationException("Failed to load version properties file"); + } + Enumeration propNames = props.propertyNames(); + while (propNames.hasMoreElements()) { + String next = (String) propNames.nextElement(); + if (next.startsWith("resource.")) { + resourceNames.add(next.substring("resource.".length()).trim()); + } + } + } + + for (RuntimeResourceDefinition next : myNameToResourceDefinition.values()) { + resourceNames.add(next.getName()); + } + + return Collections.unmodifiableSet(resourceNames); + } + /** * Get the restful client factory. If no factory has been set, this will be initialized with * a new ApacheRestfulClientFactory. - * + * * @return the factory used to create the restful clients */ public IRestfulClientFactory getRestfulClientFactory() { @@ -429,6 +529,16 @@ public class FhirContext { return myRestfulClientFactory; } + /** + * Set the restful client factory + * + * @param theRestfulClientFactory + */ + public void setRestfulClientFactory(IRestfulClientFactory theRestfulClientFactory) { + Validate.notNull(theRestfulClientFactory, "theRestfulClientFactory must not be null"); + this.myRestfulClientFactory = theRestfulClientFactory; + } + public RuntimeChildUndeclaredExtensionDefinition getRuntimeChildUndeclaredExtensionDefinition() { validateInitialized(); return myRuntimeChildUndeclaredExtensionDefinition; @@ -438,7 +548,7 @@ public class FhirContext { * Returns the validation support module configured for this context, creating a default * implementation if no module has been passed in via the {@link #setValidationSupport(IContextValidationSupport)} * method - * + * * @see #setValidationSupport(IContextValidationSupport) */ public IContextValidationSupport getValidationSupport() { @@ -448,6 +558,15 @@ public class FhirContext { return myValidationSupport; } + /** + * Sets the validation support module to use for this context. The validation support module + * is used to supply underlying infrastructure such as conformance resources (StructureDefinition, ValueSet, etc) + * as well as to provide terminology services to modules such as the validator and FluentPath executor + */ + public void setValidationSupport(IContextValidationSupport theValidationSupport) { + myValidationSupport = theValidationSupport; + } + public IFhirVersion getVersion() { return myVersion; } @@ -455,7 +574,7 @@ public class FhirContext { /** * Returns true if any default types for specific profiles have been defined * within this context. - * + * * @see #setDefaultTypeForProfile(String, Class) * @see #getDefaultTypeForProfile(String) */ @@ -483,7 +602,7 @@ public class FhirContext { * on a context for a previous version of fhir will result in an * {@link UnsupportedOperationException} *

- * + * * @since 2.2 */ public IFluentPath newFluentPath() { @@ -492,7 +611,7 @@ public class FhirContext { /** * Create and return a new JSON parser. - * + * *

* Thread safety: Parsers are not guaranteed to be thread safe. Create a new parser instance for every thread * or every message being parsed/encoded. @@ -513,19 +632,16 @@ public class FhirContext { * sub-interface {@link IBasicClient}). See the RESTful Client documentation for more * information on how to define this interface. - * + * *

* Performance Note: This method is cheap to call, and may be called once for every operation invocation * without incurring any performance penalty *

- * - * @param theClientType - * The client type, which is an interface type to be instantiated - * @param theServerBase - * The URL of the base for the restful FHIR server to connect to + * + * @param theClientType The client type, which is an interface type to be instantiated + * @param theServerBase The URL of the base for the restful FHIR server to connect to * @return A newly created client - * @throws ConfigurationException - * If the interface type is not an interface + * @throws ConfigurationException If the interface type is not an interface */ public T newRestfulClient(Class theClientType, String theServerBase) { return getRestfulClientFactory().newClient(theClientType, theServerBase); @@ -535,14 +651,13 @@ public class FhirContext { * Instantiates a new generic client. A generic client is able to perform any of the FHIR RESTful operations against * a compliant server, but does not have methods defining the specific functionality required (as is the case with * {@link #newRestfulClient(Class, String) non-generic clients}). - * + * *

* Performance Note: This method is cheap to call, and may be called once for every operation invocation * without incurring any performance penalty *

- * - * @param theServerBase - * The URL of the base for the restful FHIR server to connect to + * + * @param theServerBase The URL of the base for the restful FHIR server to connect to */ public IGenericClient newRestfulGenericClient(String theServerBase) { return getRestfulClientFactory().newGenericClient(theServerBase); @@ -569,7 +684,7 @@ public class FhirContext { /** * Create and return a new XML parser. - * + * *

* Thread safety: Parsers are not guaranteed to be thread safe. Create a new parser instance for every thread * or every message being parsed/encoded. @@ -592,9 +707,8 @@ public class FhirContext { * THREAD SAFETY WARNING: This method is not thread safe. It should be called before any * threads are able to call any methods on this context. *

- * - * @param theType - * The custom type to add (must not be null) + * + * @param theType The custom type to add (must not be null) */ public void registerCustomType(Class theType) { Validate.notNull(theType, "theType must not be null"); @@ -612,9 +726,8 @@ public class FhirContext { * THREAD SAFETY WARNING: This method is not thread safe. It should be called before any * threads are able to call any methods on this context. *

- * - * @param theTypes - * The custom types to add (must not be null or contain null elements in the collection) + * + * @param theTypes The custom types to add (must not be null or contain null elements in the collection) */ public void registerCustomTypes(Collection> theTypes) { Validate.notNull(theTypes, "theTypes must not be null"); @@ -698,31 +811,6 @@ public class FhirContext { return classToElementDefinition; } - /** - * When encoding resources, this setting configures the parser to include - * an entry in the resource's metadata section which indicates which profile(s) the - * resource claims to conform to. The default is {@link AddProfileTagEnum#ONLY_FOR_CUSTOM}. - *

- * This feature is intended for situations where custom resource types are being used, - * avoiding the need to manually add profile declarations for these custom types. - *

- *

- * See Profiling and Extensions - * for more information on using custom types. - *

- *

- * Note that this feature automatically adds the profile, but leaves any profile tags - * which have been manually added in place as well. - *

- * - * @param theAddProfileTagWhenEncoding - * The add profile mode (must not be null) - */ - public void setAddProfileTagWhenEncoding(AddProfileTagEnum theAddProfileTagWhenEncoding) { - Validate.notNull(theAddProfileTagWhenEncoding, "theAddProfileTagWhenEncoding must not be null"); - myAddProfileTagWhenEncoding = theAddProfileTagWhenEncoding; - } - /** * Sets the default type which will be used when parsing a resource that is found to be * of the given profile. @@ -732,12 +820,10 @@ public class FhirContext { * if the parser is parsing a resource and finds that it declares that it conforms to that profile, * the MyPatient type will be used unless otherwise specified. *

- * - * @param theProfile - * The profile string, e.g. "http://example.com/some_patient_profile". Must not be - * null or empty. - * @param theClass - * The resource type, or null to clear any existing type + * + * @param theProfile The profile string, e.g. "http://example.com/some_patient_profile". Must not be + * null or empty. + * @param theClass The resource type, or null to clear any existing type */ public void setDefaultTypeForProfile(String theProfile, Class theClass) { Validate.notBlank(theProfile, "theProfile must not be null or empty"); @@ -748,56 +834,19 @@ public class FhirContext { } } - /** - * This feature is not yet in its final state and should be considered an internal part of HAPI for now - use with - * caution - */ - public void setLocalizer(HapiLocalizer theMessages) { - myLocalizer = theMessages; - } - - public void setNarrativeGenerator(INarrativeGenerator theNarrativeGenerator) { - myNarrativeGenerator = theNarrativeGenerator; - } - /** * Sets a parser error handler to use by default on all parsers - * - * @param theParserErrorHandler - * The error handler + * + * @param theParserErrorHandler The error handler */ public void setParserErrorHandler(IParserErrorHandler theParserErrorHandler) { Validate.notNull(theParserErrorHandler, "theParserErrorHandler must not be null"); myParserErrorHandler = theParserErrorHandler; } - /** - * Sets the parser options object which will be used to supply default - * options to newly created parsers - * - * @param theParserOptions - * The parser options object - Must not be null - */ - public void setParserOptions(ParserOptions theParserOptions) { - Validate.notNull(theParserOptions, "theParserOptions must not be null"); - myParserOptions = theParserOptions; - } - /** * Sets the configured performance options - * - * @see PerformanceOptionsEnum for a list of available options - */ - public void setPerformanceOptions(Collection theOptions) { - myPerformanceOptions.clear(); - if (theOptions != null) { - myPerformanceOptions.addAll(theOptions); - } - } - - /** - * Sets the configured performance options - * + * * @see PerformanceOptionsEnum for a list of available options */ public void setPerformanceOptions(PerformanceOptionsEnum... thePerformanceOptions) { @@ -808,26 +857,7 @@ public class FhirContext { setPerformanceOptions(asList); } - /** - * Set the restful client factory - * - * @param theRestfulClientFactory - */ - public void setRestfulClientFactory(IRestfulClientFactory theRestfulClientFactory) { - Validate.notNull(theRestfulClientFactory, "theRestfulClientFactory must not be null"); - this.myRestfulClientFactory = theRestfulClientFactory; - } - - /** - * Sets the validation support module to use for this context. The validation support module - * is used to supply underlying infrastructure such as conformance resources (StructureDefinition, ValueSet, etc) - * as well as to provide terminology services to modules such as the validator and FluentPath executor - */ - public void setValidationSupport(IContextValidationSupport theValidationSupport) { - myValidationSupport = theValidationSupport; - } - - @SuppressWarnings({ "cast" }) + @SuppressWarnings({"cast"}) private List> toElementList(Collection> theResourceTypes) { if (theResourceTypes == null) { return null; @@ -858,13 +888,6 @@ public class FhirContext { return new FhirContext(FhirVersionEnum.DSTU2); } - /** - * Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU2 DSTU2} (2016 May DSTU3 Snapshot) - */ - public static FhirContext forDstu2_1() { - return new FhirContext(FhirVersionEnum.DSTU2_1); - } - /** * Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU2_HL7ORG DSTU2} (using the Reference * Implementation Structures) @@ -873,9 +896,16 @@ public class FhirContext { return new FhirContext(FhirVersionEnum.DSTU2_HL7ORG); } + /** + * Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU2 DSTU2} (2016 May DSTU3 Snapshot) + */ + public static FhirContext forDstu2_1() { + return new FhirContext(FhirVersionEnum.DSTU2_1); + } + /** * Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU3 DSTU3} - * + * * @since 1.4 */ public static FhirContext forDstu3() { @@ -884,14 +914,13 @@ public class FhirContext { /** * Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU3 DSTU3} - * + * * @since 3.0.0 */ public static FhirContext forR4() { return new FhirContext(FhirVersionEnum.R4); } - private static Collection> toCollection(Class theResourceType) { ArrayList> retVal = new ArrayList>(1); retVal.add(theResourceType); @@ -909,34 +938,4 @@ public class FhirContext { } return retVal; } - - /** - * Returns an unmodifiable set containing all resource names known to this - * context - */ - public Set getResourceNames() { - Set resourceNames= new HashSet<>(); - - if (myNameToResourceDefinition.isEmpty()) { - Properties props = new Properties(); - try { - props.load(myVersion.getFhirVersionPropertiesFile()); - } catch (IOException theE) { - throw new ConfigurationException("Failed to load version properties file"); - } - Enumeration propNames = props.propertyNames(); - while (propNames.hasMoreElements()){ - String next = (String) propNames.nextElement(); - if (next.startsWith("resource.")) { - resourceNames.add(next.substring("resource.".length()).trim()); - } - } - } - - for (RuntimeResourceDefinition next : myNameToResourceDefinition.values()) { - resourceNames.add(next.getName()); - } - - return Collections.unmodifiableSet(resourceNames); - } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceDefinition.java index 93330b674b5..7ef427cbbf2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeResourceDefinition.java @@ -185,14 +185,29 @@ public class RuntimeResourceDefinition extends BaseRuntimeElementCompositeDefini }); mySearchParams = Collections.unmodifiableList(searchParams); - Map> compartmentNameToSearchParams = new HashMap>(); + Map> compartmentNameToSearchParams = new HashMap<>(); for (RuntimeSearchParam next : searchParams) { if (next.getProvidesMembershipInCompartments() != null) { for (String nextCompartment : next.getProvidesMembershipInCompartments()) { if (!compartmentNameToSearchParams.containsKey(nextCompartment)) { - compartmentNameToSearchParams.put(nextCompartment, new ArrayList()); + compartmentNameToSearchParams.put(nextCompartment, new ArrayList<>()); + } + List searchParamsForCompartment = compartmentNameToSearchParams.get(nextCompartment); + searchParamsForCompartment.add(next); + + /* + * If one search parameter marks an SP as making a resource + * a part of a compartment, let's also denote all other + * SPs with the same path the same way. This behaviour is + * used by AuthorizationInterceptor + */ + for (RuntimeSearchParam nextAlternate : searchParams) { + if (nextAlternate.getPath().equals(next.getPath())) { + if (!nextAlternate.getName().equals(next.getName())) { + searchParamsForCompartment.add(nextAlternate); + } + } } - compartmentNameToSearchParams.get(nextCompartment).add(next); } } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java index e1e02f2f9f2..2c1ae3dc023 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeSearchParam.java @@ -5,6 +5,10 @@ import static org.apache.commons.lang3.StringUtils.trim; import java.util.*; +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 org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; @@ -38,6 +42,18 @@ public class RuntimeSearchParam { private final RestSearchParameterTypeEnum myParamType; private final String myPath; private final Set myTargets; + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("base", myBase) + .append("name", myName) + .append("path", myPath) + .append("id", myId) + .append("uri", myUri) + .toString(); + } + private final Set myProvidesMembershipInCompartments; private final RuntimeSearchParamStatusEnum myStatus; private final String myUri; @@ -55,9 +71,36 @@ public class RuntimeSearchParam { this(theId, theUri, theName, theDescription, thePath, theParamType, theCompositeOf, theProvidesMembershipInCompartments, theTargets, theStatus, null); } + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + + if (theO == null || getClass() != theO.getClass()) return false; + + RuntimeSearchParam that = (RuntimeSearchParam) theO; + + return new EqualsBuilder() + .append(getId(), that.getId()) + .append(getName(), that.getName()) + .append(getPath(), that.getPath()) + .append(getUri(), that.getUri()) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(getId()) + .append(getName()) + .append(getPath()) + .append(getUri()) + .toHashCode(); + } + public RuntimeSearchParam(IIdType theId, String theUri, String theName, String theDescription, String thePath, RestSearchParameterTypeEnum theParamType, List theCompositeOf, - Set theProvidesMembershipInCompartments, Set theTargets, RuntimeSearchParamStatusEnum theStatus, Collection theBase) { + Set theProvidesMembershipInCompartments, Set theTargets, RuntimeSearchParamStatusEnum theStatus, Collection theBase) { super(); + myId = theId; myUri = theUri; myName = theName; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/EncodingEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/EncodingEnum.java index 4a6e398198c..d4b450aeff8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/EncodingEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/EncodingEnum.java @@ -156,7 +156,12 @@ public enum EncodingEnum { *

*/ public static EncodingEnum forContentType(String theContentType) { - return ourContentTypeToEncoding.get(theContentType); + String contentTypeSplitted = getTypeWithoutCharset(theContentType); + if (contentTypeSplitted == null) { + return null; + } else { + return ourContentTypeToEncoding.get(contentTypeSplitted ); + } } @@ -170,14 +175,33 @@ public enum EncodingEnum { * @see #forContentType(String) */ public static EncodingEnum forContentTypeStrict(String theContentType) { - return ourContentTypeToEncodingStrict.get(theContentType); + String contentTypeSplitted = getTypeWithoutCharset(theContentType); + if (contentTypeSplitted == null) { + return null; + } else { + return ourContentTypeToEncodingStrict.get(contentTypeSplitted); + } + } + + private static String getTypeWithoutCharset(String theContentType) { + if (theContentType == null) { + return null; + } else { + String[] contentTypeSplitted = theContentType.split(";"); + return contentTypeSplitted[0]; + } } /** * Is the given type a FHIR legacy (pre-DSTU3) content type? */ - public static boolean isLegacy(String theFormat) { - return ourContentTypeToEncodingLegacy.containsKey(theFormat); + public static boolean isLegacy(String theContentType) { + String contentTypeSplitted = getTypeWithoutCharset(theContentType); + if (contentTypeSplitted == null) { + return false; + } else { + return ourContentTypeToEncodingLegacy.containsKey(contentTypeSplitted); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlPathTokenizer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlPathTokenizer.java index 0e60d47a30f..7c7a99f2915 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlPathTokenizer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlPathTokenizer.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.util; * 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. @@ -34,8 +34,17 @@ public class UrlPathTokenizer { return myTok.hasMoreTokens(); } - public String nextToken() { - return UrlUtil.unescape(myTok.nextToken()); + /** + * Returns the next portion. Any URL-encoding is undone, but we will + * HTML encode the < and " marks since they are both + * not useful un URL paths in FHIR and potentially represent injection + * attacks. + * + * @see UrlUtil#sanitizeUrlPart(String) + * @see UrlUtil#unescape(String) + */ + public String nextTokenUnescapedAndSanitized() { + return UrlUtil.sanitizeUrlPart(UrlUtil.unescape(myTok.nextToken())); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java index 627b6efefa6..5061148b7c1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java @@ -25,9 +25,9 @@ import static org.apache.commons.lang3.StringUtils.isBlank; * 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. @@ -70,7 +70,7 @@ public class UrlUtil { return theExtensionUrl; } if (theExtensionUrl == null) { - return theExtensionUrl; + return null; } int parentLastSlashIdx = theParentExtensionUrl.lastIndexOf('/'); @@ -119,6 +119,18 @@ public class UrlUtil { return value.startsWith("http://") || value.startsWith("https://"); } + public static boolean isNeedsSanitization(String theString) { + if (theString != null) { + for (int i = 0; i < theString.length(); i++) { + char nextChar = theString.charAt(i); + if (nextChar == '<' || nextChar == '"') { + return true; + } + } + } + return false; + } + public static boolean isValid(String theUrl) { if (theUrl == null || theUrl.length() < 8) { return false; @@ -164,7 +176,7 @@ public class UrlUtil { } public static Map parseQueryString(String theQueryString) { - HashMap> map = new HashMap>(); + HashMap> map = new HashMap<>(); parseQueryString(theQueryString, map); return toQueryStringMap(map); } @@ -197,17 +209,13 @@ public class UrlUtil { nextKey = unescape(nextKey); nextValue = unescape(nextValue); - List list = map.get(nextKey); - if (list == null) { - list = new ArrayList<>(); - map.put(nextKey, list); - } + List list = map.computeIfAbsent(nextKey, k -> new ArrayList<>()); list.add(nextValue); } } public static Map parseQueryStrings(String... theQueryString) { - HashMap> map = new HashMap>(); + HashMap> map = new HashMap<>(); for (String next : theQueryString) { parseQueryString(next, map); } @@ -222,7 +230,6 @@ public class UrlUtil { *
  • [Resource Type]/[Resource ID]/_history/[Version ID] * */ - //@formatter:on public static UrlParts parseUrl(String theUrl) { String url = theUrl; UrlParts retVal = new UrlParts(); @@ -243,7 +250,7 @@ public class UrlUtil { retVal.setVersionId(id.getVersionIdPart()); return retVal; } - if (url.matches("\\/[a-zA-Z]+\\?.*")) { + if (url.matches("/[a-zA-Z]+\\?.*")) { url = url.substring(1); } int nextStart = 0; @@ -282,12 +289,47 @@ public class UrlUtil { } - //@formatter:off + /** + * This method specifically HTML-encodes the " and + * < characters in order to prevent injection attacks + */ + public static String sanitizeUrlPart(String theString) { + if (theString == null) { + return null; + } + + boolean needsSanitization = isNeedsSanitization(theString); + + if (needsSanitization) { + // Ok, we're sanitizing + StringBuilder buffer = new StringBuilder(theString.length() + 10); + for (int j = 0; j < theString.length(); j++) { + + char nextChar = theString.charAt(j); + switch (nextChar) { + case '"': + buffer.append("""); + break; + case '<': + buffer.append("<"); + break; + default: + buffer.append(nextChar); + break; + } + + } // for build escaped string + + return buffer.toString(); + } + + return theString; + } private static Map toQueryStringMap(HashMap> map) { - HashMap retVal = new HashMap(); + HashMap retVal = new HashMap<>(); for (Entry> nextEntry : map.entrySet()) { - retVal.put(nextEntry.getKey(), nextEntry.getValue().toArray(new String[nextEntry.getValue().size()])); + retVal.put(nextEntry.getKey(), nextEntry.getValue().toArray(new String[0])); } return retVal; } diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java index 756567ed7d4..5179a03afbc 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java @@ -28,9 +28,6 @@ import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import org.hl7.fhir.dstu3.model.Parameters; -import org.hl7.fhir.dstu3.model.StringType; -import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.instance.model.api.IBaseParameters; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -82,10 +79,17 @@ public class UploadTerminologyCommand extends BaseCommand { IGenericClient client = super.newClient(theCommandLine); IBaseParameters inputParameters; if (ctx.getVersion().getVersion() == FhirVersionEnum.DSTU3) { - Parameters p = new Parameters(); - p.addParameter().setName("url").setValue(new UriType(termUrl)); + org.hl7.fhir.dstu3.model.Parameters p = new org.hl7.fhir.dstu3.model.Parameters(); + p.addParameter().setName("url").setValue(new org.hl7.fhir.dstu3.model.UriType(termUrl)); for (String next : datafile) { - p.addParameter().setName("localfile").setValue(new StringType(next)); + p.addParameter().setName("localfile").setValue(new org.hl7.fhir.dstu3.model.StringType(next)); + } + inputParameters = p; + } else if (ctx.getVersion().getVersion() == FhirVersionEnum.R4) { + org.hl7.fhir.r4.model.Parameters p = new org.hl7.fhir.r4.model.Parameters(); + p.addParameter().setName("url").setValue(new org.hl7.fhir.r4.model.UriType(termUrl)); + for (String next : datafile) { + p.addParameter().setName("localfile").setValue(new org.hl7.fhir.r4.model.StringType(next)); } inputParameters = p; } else { diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/resources/logback-cli-on.xml b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/resources/logback-cli-on.xml index 9a96c590af6..730679bd5bb 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/resources/logback-cli-on.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/resources/logback-cli-on.xml @@ -31,6 +31,13 @@ + + + + + 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 5dd55c8bf31..000e0eb2a48 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 @@ -1103,7 +1103,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings("unchecked") @Override public Object execute() { - if (myOperationName != null && myOperationName.equals(Constants.EXTOP_PROCESS_MESSAGE)) { + if (myOperationName != null && myOperationName.equals(Constants.EXTOP_PROCESS_MESSAGE) && myMsgBundle != null) { Map> urlParams = new LinkedHashMap>(); // Set Url parameter Async and Response-Url if (myIsAsync != null) { diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseHttpClientInvocationWithContents.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseHttpClientInvocationWithContents.java index 8dc61aa7d0d..86724743f68 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseHttpClientInvocationWithContents.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseHttpClientInvocationWithContents.java @@ -57,17 +57,6 @@ abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvoca private IIdType myForceResourceId; - - public BaseHttpClientInvocationWithContents(FhirContext theContext, IBaseResource theResource, Map> theParams, String... theUrlPath) { - super(theContext); - myResource = theResource; - myUrlPath = StringUtils.join(theUrlPath, '/'); - myResources = null; - myContents = null; - myParams = theParams; - myBundleType = null; - } - public BaseHttpClientInvocationWithContents(FhirContext theContext, IBaseResource theResource, String theUrlPath) { super(theContext); myResource = theResource; @@ -105,17 +94,6 @@ abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvoca myBundleType = null; } - public BaseHttpClientInvocationWithContents(FhirContext theContext, String theContents, Map> theParams, String... theUrlPath) { - super(theContext); - myResource = null; - myUrlPath = StringUtils.join(theUrlPath, '/'); - myResources = null; - myContents = theContents; - myParams = theParams; - myBundleType = null; - } - - @Override public IHttpRequest asHttpRequest(String theUrlBase, Map> theExtraParams, EncodingEnum theEncoding, Boolean thePrettyPrint) throws DataFormatException { StringBuilder url = new StringBuilder(); diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index a267db49303..0954f47e0de 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -502,6 +502,7 @@ com.github.ben-manes.caffeine caffeine + com.google.guava guava-testlib 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 80d5a91471b..9da25a9f395 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 @@ -8,10 +8,10 @@ import ca.uhn.fhir.jpa.util.ResourceCountCache; import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.validation.IValidatorModule; import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.instance.hapi.validation.CachingValidationSupport; import org.hl7.fhir.instance.hapi.validation.DefaultProfileValidationSupport; import org.hl7.fhir.instance.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.instance.hapi.validation.ValidationSupportChain; -import org.hl7.fhir.instance.utils.IResourceValidator.BestPracticeWarningLevel; import org.hl7.fhir.r4.utils.IResourceValidator; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.context.annotation.Bean; @@ -81,7 +81,7 @@ public class BaseDstu2Config extends BaseConfig { public IValidatorModule instanceValidatorDstu2() { FhirInstanceValidator retVal = new FhirInstanceValidator(); retVal.setBestPracticeWarningLevel(IResourceValidator.BestPracticeWarningLevel.Warning); - retVal.setValidationSupport(new ValidationSupportChain(new DefaultProfileValidationSupport(), jpaValidationSupportDstu2())); + retVal.setValidationSupport(new CachingValidationSupport(new ValidationSupportChain(new DefaultProfileValidationSupport(), jpaValidationSupportDstu2()))); return retVal; } @@ -91,6 +91,13 @@ public class BaseDstu2Config extends BaseConfig { return retVal; } + @Bean(name = "myResourceCountsCache") + public ResourceCountCache resourceCountsCache() { + ResourceCountCache retVal = new ResourceCountCache(() -> systemDaoDstu2().getResourceCounts()); + retVal.setCacheMillis(60 * DateUtils.MILLIS_PER_SECOND); + return retVal; + } + @Bean(autowire = Autowire.BY_TYPE) public IFulltextSearchSvc searchDao() { FulltextSearchSvcImpl searchDao = new FulltextSearchSvcImpl(); @@ -121,13 +128,6 @@ public class BaseDstu2Config extends BaseConfig { return retVal; } - @Bean(name = "myResourceCountsCache") - public ResourceCountCache resourceCountsCache() { - ResourceCountCache retVal = new ResourceCountCache(() -> systemDaoDstu2().getResourceCounts()); - retVal.setCacheMillis(60 * DateUtils.MILLIS_PER_SECOND); - return retVal; - } - @Bean(autowire = Autowire.BY_TYPE) public IHapiTerminologySvc terminologyService() { return new HapiTerminologySvcDstu2(); 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 44314f57377..3ec6c62431e 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 @@ -19,6 +19,7 @@ import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3; import ca.uhn.fhir.validation.IValidatorModule; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; +import org.hl7.fhir.dstu3.hapi.validation.CachingValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.r4.utils.IResourceValidator; import org.springframework.beans.factory.annotation.Autowire; @@ -78,13 +79,17 @@ public class BaseDstu3Config extends BaseConfig { return val; } + @Bean + public JpaValidationSupportChainDstu3 jpaValidationSupportChain() { + return new JpaValidationSupportChainDstu3(); + } + @Bean(name = "myJpaValidationSupportDstu3", autowire = Autowire.BY_NAME) public ca.uhn.fhir.jpa.dao.dstu3.IJpaValidationSupportDstu3 jpaValidationSupportDstu3() { ca.uhn.fhir.jpa.dao.dstu3.JpaValidationSupportDstu3 retVal = new ca.uhn.fhir.jpa.dao.dstu3.JpaValidationSupportDstu3(); return retVal; } - @Bean(name = "myResourceCountsCache") public ResourceCountCache resourceCountsCache() { ResourceCountCache retVal = new ResourceCountCache(() -> systemDaoDstu3().getResourceCounts()); @@ -142,7 +147,7 @@ public class BaseDstu3Config extends BaseConfig { @Primary @Bean(autowire = Autowire.BY_NAME, name = "myJpaValidationSupportChainDstu3") public IValidationSupport validationSupportChainDstu3() { - return new JpaValidationSupportChainDstu3(); + return new CachingValidationSupport(jpaValidationSupportChain()); } } 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 dced4a6f8aa..df65c52dafe 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 @@ -21,6 +21,7 @@ import ca.uhn.fhir.validation.IValidatorModule; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider; +import org.hl7.fhir.r4.hapi.validation.CachingValidationSupport; import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.r4.utils.GraphQLEngine; import org.hl7.fhir.r4.utils.IResourceValidator.BestPracticeWarningLevel; @@ -93,6 +94,11 @@ public class BaseR4Config extends BaseConfig { return val; } + @Bean + public JpaValidationSupportChainR4 jpaValidationSupportChain() { + return new JpaValidationSupportChainR4(); + } + @Bean(name = "myJpaValidationSupportR4", autowire = Autowire.BY_NAME) public ca.uhn.fhir.jpa.dao.r4.IJpaValidationSupportR4 jpaValidationSupportR4() { ca.uhn.fhir.jpa.dao.r4.JpaValidationSupportR4 retVal = new ca.uhn.fhir.jpa.dao.r4.JpaValidationSupportR4(); @@ -156,7 +162,7 @@ public class BaseR4Config extends BaseConfig { @Primary @Bean(autowire = Autowire.BY_NAME, name = "myJpaValidationSupportChainR4") public IValidationSupport validationSupportChainR4() { - return new JpaValidationSupportChainR4(); + return new CachingValidationSupport(jpaValidationSupportChain()); } } 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 016ad9fa615..4b643375b78 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,25 +1,5 @@ package ca.uhn.fhir.jpa.dao; -/* - * #%L - * HAPI FHIR JPA Server - * %% - * Copyright (C) 2014 - 2018 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - import ca.uhn.fhir.context.*; import ca.uhn.fhir.jpa.dao.data.*; import ca.uhn.fhir.jpa.entity.*; @@ -58,7 +38,6 @@ import ca.uhn.fhir.util.*; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; @@ -104,6 +83,26 @@ import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.*; +/* + * #%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% + */ + @SuppressWarnings("WeakerAccess") @Repository public abstract class BaseHapiFhirDao implements IDao, ApplicationContextAware { @@ -186,6 +185,8 @@ public abstract class BaseHapiFhirDao implements IDao, protected IResourceTableDao myResourceTableDao; @Autowired protected IResourceTagDao myResourceTagDao; + @Autowired + protected IResourceSearchViewDao myResourceViewDao; @Autowired(required = true) private DaoConfig myConfig; private FhirContext myContext; @@ -199,8 +200,8 @@ public abstract class BaseHapiFhirDao implements IDao, private ISearchParamPresenceSvc mySearchParamPresenceSvc; @Autowired private ISearchParamRegistry mySearchParamRegistry; - @Autowired - private ISearchResultDao mySearchResultDao; + //@Autowired + //private ISearchResultDao mySearchResultDao; @Autowired private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; private ApplicationContext myApplicationContext; @@ -227,6 +228,7 @@ public abstract class BaseHapiFhirDao implements IDao, } protected ExpungeOutcome doExpunge(String theResourceName, Long theResourceId, Long theVersion, ExpungeOptions theExpungeOptions) { + TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); if (!getConfig().isExpungeEnabled()) { throw new MethodNotAllowedException("$expunge is not enabled on this server"); @@ -245,32 +247,39 @@ public abstract class BaseHapiFhirDao implements IDao, /* * Delete historical versions of deleted resources */ - Pageable page = new PageRequest(0, remainingCount.get()); - Slice resourceIds; - if (theResourceId != null) { - resourceIds = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceId, theResourceName); - } else { - if (theResourceName != null) { - resourceIds = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceName); + Pageable page = PageRequest.of(0, remainingCount.get()); + Slice resourceIds = txTemplate.execute(t -> { + if (theResourceId != null) { + return myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceId, theResourceName); } else { - resourceIds = myResourceTableDao.findIdsOfDeletedResources(page); + if (theResourceName != null) { + return myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceName); + } else { + return myResourceTableDao.findIdsOfDeletedResources(page); + } } - } + }); for (Long next : resourceIds) { - expungeHistoricalVersionsOfId(next, remainingCount); - if (remainingCount.get() <= 0) { - return toExpungeOutcome(theExpungeOptions, remainingCount); - } + txTemplate.execute(t -> { + expungeHistoricalVersionsOfId(next, remainingCount); + if (remainingCount.get() <= 0) { + return toExpungeOutcome(theExpungeOptions, remainingCount); + } + return null; + }); } /* * Delete current versions of deleted resources */ for (Long next : resourceIds) { - expungeCurrentVersionOfResource(next); - if (remainingCount.get() <= 0) { - return toExpungeOutcome(theExpungeOptions, remainingCount); - } + txTemplate.execute(t -> { + expungeCurrentVersionOfResource(next); + if (remainingCount.get() <= 0) { + return toExpungeOutcome(theExpungeOptions, remainingCount); + } + return null; + }); } } @@ -280,22 +289,26 @@ public abstract class BaseHapiFhirDao implements IDao, /* * Delete historical versions of non-deleted resources */ - Pageable page = new PageRequest(0, remainingCount.get()); - Slice historicalIds; - if (theResourceId != null && theVersion != null) { - historicalIds = toSlice(myResourceHistoryTableDao.findForIdAndVersion(theResourceId, theVersion)); - } else { - if (theResourceName != null) { - historicalIds = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName); + Pageable page = PageRequest.of(0, remainingCount.get()); + Slice historicalIds = txTemplate.execute(t -> { + if (theResourceId != null && theVersion != null) { + return toSlice(myResourceHistoryTableDao.findForIdAndVersion(theResourceId, theVersion)); } else { - historicalIds = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page); + if (theResourceName != null) { + return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName); + } else { + return myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page); + } } - } + }); for (Long next : historicalIds) { - expungeHistoricalVersion(next); - if (remainingCount.decrementAndGet() <= 0) { - return toExpungeOutcome(theExpungeOptions, remainingCount); - } + txTemplate.execute(t -> { + expungeHistoricalVersion(next); + if (remainingCount.decrementAndGet() <= 0) { + return toExpungeOutcome(theExpungeOptions, remainingCount); + } + return null; + }); } } @@ -315,7 +328,6 @@ public abstract class BaseHapiFhirDao implements IDao, }); txTemplate.execute(t -> { doExpungeEverythingQuery("DELETE from " + SearchParamPresent.class.getSimpleName() + " d"); - doExpungeEverythingQuery("DELETE from " + SearchParam.class.getSimpleName() + " d"); doExpungeEverythingQuery("DELETE from " + ForcedId.class.getSimpleName() + " d"); doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamDate.class.getSimpleName() + " d"); doExpungeEverythingQuery("DELETE from " + ResourceIndexedSearchParamNumber.class.getSimpleName() + " d"); @@ -704,58 +716,6 @@ public abstract class BaseHapiFhirDao implements IDao, return retVal; } - - @SuppressWarnings("unchecked") - public IFhirResourceDao getDao(Class theType) { - Map, IFhirResourceDao> resourceTypeToDao = getDaos(); - IFhirResourceDao dao = (IFhirResourceDao) resourceTypeToDao.get(theType); - return dao; - } - - 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); - } - - @PostConstruct - public void startClearCaches() { - myResourceTypeToDao = null; - } - - protected Set extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) { return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource); } @@ -910,7 +870,8 @@ public abstract class BaseHapiFhirDao implements IDao, param = new ResourceIndexedSearchParamQuantity(); break; case STRING: - param = new ResourceIndexedSearchParamString(); + param = new ResourceIndexedSearchParamString() + .setDaoConfig(myConfig); break; case TOKEN: param = new ResourceIndexedSearchParamToken(); @@ -957,18 +918,6 @@ public abstract class BaseHapiFhirDao implements IDao, return myConfig; } - @Override - public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException { - /* - * We do a null check here because Smile's module system tries to - * initialize the application context twice if two modules depend on - * the persistence module. The second time sets the dependency's appctx. - */ - if (myApplicationContext == null) { - myApplicationContext = theApplicationContext; - } - } - public void setConfig(DaoConfig theConfig) { myConfig = theConfig; } @@ -995,6 +944,50 @@ 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; + } + + 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); + } + public IResourceIndexedCompositeStringUniqueDao getResourceIndexedCompositeStringUniqueDao() { return myResourceIndexedCompositeStringUniqueDao; } @@ -1172,9 +1165,9 @@ public abstract class BaseHapiFhirDao implements IDao, @Override public SearchBuilder newSearchBuilder() { - SearchBuilder builder = new SearchBuilder(getContext(), myEntityManager, myFulltextSearchSvc, this, myResourceIndexedSearchParamUriDao, - myForcedIdDao, - myTerminologySvc, mySerarchParamRegistry); + SearchBuilder builder = new SearchBuilder( + getContext(), myEntityManager, myFulltextSearchSvc, this, myResourceIndexedSearchParamUriDao, + myForcedIdDao, myTerminologySvc, mySerarchParamRegistry, myResourceTagDao, myResourceViewDao); return builder; } @@ -1223,7 +1216,7 @@ public abstract class BaseHapiFhirDao implements IDao, } } - private void populateResourceIdFromEntity(BaseHasResource theEntity, final IBaseResource theResource) { + private void populateResourceIdFromEntity(IBaseResourceEntity theEntity, final IBaseResource theResource) { IIdType id = theEntity.getIdDt(); if (getContext().getVersion().getVersion().isRi()) { id = getContext().getVersion().newIdType().setValue(id.getValue()); @@ -1308,20 +1301,24 @@ public abstract class BaseHapiFhirDao implements IDao, } } - // Don't keep duplicate tags + Set allTagsNew = getAllTagDefinitions(theEntity); Set allDefsPresent = new HashSet<>(); - theEntity.getTags().removeIf(theResourceTag -> !allDefsPresent.add(theResourceTag.getTag())); + allTagsNew.forEach(tag -> { - // Remove any tags that have been removed - for (ResourceTag next : allTagsOld) { - if (!allDefs.contains(next)) { - if (shouldDroppedTagBeRemovedOnUpdate(theRequest, next)) { - theEntity.getTags().remove(next); + // Don't keep duplicate tags + if (!allDefsPresent.add(tag.getTag())) { + theEntity.getTags().remove(tag); + } + + // Drop any tags that have been removed + if (!allDefs.contains(tag)) { + if (shouldDroppedTagBeRemovedOnUpdate(theRequest, tag)) { + theEntity.getTags().remove(tag); } } - } - Set allTagsNew = getAllTagDefinitions(theEntity); + }); + if (!allTagsOld.equals(allTagsNew)) { changed = true; } @@ -1355,7 +1352,7 @@ public abstract class BaseHapiFhirDao implements IDao, } @SuppressWarnings("unchecked") - private R populateResourceMetadataHapi(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation, IResource res) { + private R populateResourceMetadataHapi(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation, IResource res) { R retVal = (R) res; if (theEntity.getDeleted() != null) { res = (IResource) myContext.getResourceDefinition(theResourceType).newInstance(); @@ -1384,7 +1381,7 @@ public abstract class BaseHapiFhirDao implements IDao, ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated()); IDao.RESOURCE_PID.put(res, theEntity.getId()); - Collection tags = theEntity.getTags(); + Collection tags = theTagList; if (theEntity.isHasTags()) { TagList tagList = new TagList(); List securityLabels = new ArrayList<>(); @@ -1421,7 +1418,7 @@ public abstract class BaseHapiFhirDao implements IDao, } @SuppressWarnings("unchecked") - private R populateResourceMetadataRi(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation, IAnyResource res) { + private R populateResourceMetadataRi(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation, IAnyResource res) { R retVal = (R) res; if (theEntity.getDeleted() != null) { res = (IAnyResource) myContext.getResourceDefinition(theResourceType).newInstance(); @@ -1454,7 +1451,7 @@ public abstract class BaseHapiFhirDao implements IDao, res.getMeta().setLastUpdated(theEntity.getUpdatedDate()); IDao.RESOURCE_PID.put(res, theEntity.getId()); - Collection tags = theEntity.getTags(); + Collection tags = theTagList; if (theEntity.isHasTags()) { for (BaseTag next : tags) { @@ -1480,6 +1477,15 @@ public abstract class BaseHapiFhirDao implements IDao, return retVal; } + /** + * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database + * + * @param theEntity The resource + */ + protected void postDelete(ResourceTable theEntity) { + // nothing + } + /** * Subclasses may override to provide behaviour. Called when a resource has been inserted into the database for the first time. * @@ -1536,6 +1542,18 @@ public abstract class BaseHapiFhirDao implements IDao, return retVal; } + @Override + public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException { + /* + * We do a null check here because Smile's module system tries to + * initialize the application context twice if two modules depend on + * the persistence module. The second time sets the dependency's appctx. + */ + if (myApplicationContext == null) { + myApplicationContext = theApplicationContext; + } + } + private void setUpdatedTime(Collection theParams, Date theUpdateTime) { for (BaseResourceIndexedSearchParam nextSearchParam : theParams) { nextSearchParam.setUpdated(theUpdateTime); @@ -1592,6 +1610,11 @@ 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()); @@ -1601,28 +1624,47 @@ public abstract class BaseHapiFhirDao implements IDao, public IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation) { RuntimeResourceDefinition type = myContext.getResourceDefinition(theEntity.getResourceType()); Class resourceType = type.getImplementingClass(); - return toResource(resourceType, theEntity, theForHistoryOperation); + return toResource(resourceType, theEntity, null, theForHistoryOperation); } @SuppressWarnings("unchecked") @Override - public R toResource(Class theResourceType, BaseHasResource theEntity, - boolean theForHistoryOperation) { + public R toResource(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation) { + + // 1. get resource, it's encoding and the tags if any + byte[] resourceBytes = null; + ResourceEncodingEnum resourceEncoding = null; + Collection myTagList = null; - ResourceHistoryTable history; if (theEntity instanceof ResourceHistoryTable) { - history = (ResourceHistoryTable) theEntity; + ResourceHistoryTable history = (ResourceHistoryTable) theEntity; + resourceBytes = history.getResource(); + resourceEncoding = history.getEncoding(); + myTagList = history.getTags(); + } else if (theEntity instanceof ResourceTable) { + ResourceTable resource = (ResourceTable) theEntity; + ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion()); + if (history == null) { + return null; + } + resourceBytes = history.getResource(); + resourceEncoding = history.getEncoding(); + myTagList = resource.getTags(); + } else if (theEntity instanceof ResourceSearchView) { + // This is the search View + ResourceSearchView myView = (ResourceSearchView) theEntity; + resourceBytes = myView.getResource(); + resourceEncoding = myView.getEncoding(); + if (theTagList == null) + myTagList = new HashSet<>(); + else + myTagList = theTagList; } else { - history = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion()); - } - - if (history == null) { + // something wrong return null; } - byte[] resourceBytes = history.getResource(); - ResourceEncodingEnum resourceEncoding = history.getEncoding(); - + // 2. get The text String resourceText = null; switch (resourceEncoding) { case JSON: @@ -1639,12 +1681,10 @@ public abstract class BaseHapiFhirDao implements IDao, break; } - /* - * Use the appropriate custom type if one is specified in the context - */ + // 3. Use the appropriate custom type if one is specified in the context Class resourceType = theResourceType; if (myContext.hasDefaultTypeForProfile()) { - for (BaseTag nextTag : theEntity.getTags()) { + for (BaseTag nextTag : myTagList) { if (nextTag.getTag().getTagType() == TagTypeEnum.PROFILE) { String profile = nextTag.getTag().getCode(); if (isNotBlank(profile)) { @@ -1659,6 +1699,7 @@ public abstract class BaseHapiFhirDao implements IDao, } } + // 4. parse the text to FHIR R retVal; if (resourceEncoding != ResourceEncodingEnum.DEL) { IParser parser = resourceEncoding.newParser(getContext(theEntity.getFhirVersion())); @@ -1689,15 +1730,15 @@ public abstract class BaseHapiFhirDao implements IDao, } + // 5. fill MetaData if (retVal instanceof IResource) { IResource res = (IResource) retVal; - retVal = populateResourceMetadataHapi(resourceType, theEntity, theForHistoryOperation, res); + retVal = populateResourceMetadataHapi(resourceType, theEntity, myTagList, theForHistoryOperation, res); } else { IAnyResource res = (IAnyResource) retVal; - retVal = populateResourceMetadataRi(resourceType, theEntity, theForHistoryOperation, res); + retVal = populateResourceMetadataRi(resourceType, theEntity, myTagList, theForHistoryOperation, res); } - return retVal; } @@ -1735,8 +1776,12 @@ public abstract class BaseHapiFhirDao implements IDao, protected ResourceTable updateEntity(RequestDetails theRequest, final IBaseResource theResource, ResourceTable theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing, boolean theUpdateVersion, Date theUpdateTime, boolean theForceUpdate, boolean theCreateNewHistoryEntry) { + Validate.notNull(theEntity); + Validate.isTrue(theDeletedTimestampOrNull != null || theResource != null, "Must have either a resource[{}] or a deleted timestamp[{}] for resource PID[{}]", theDeletedTimestampOrNull != null, theResource != null, theEntity.getId()); + ourLog.debug("Starting entity update"); + /* * This should be the very first thing.. */ @@ -1826,6 +1871,7 @@ public abstract class BaseHapiFhirDao implements IDao, theEntity.setNarrativeTextParsedIntoWords(null); theEntity.setContentTextParsedIntoWords(null); theEntity.setHashSha256(null); + theEntity.setIndexStatus(INDEX_STATUS_INDEXED); changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true); } else { @@ -2003,6 +2049,11 @@ public abstract class BaseHapiFhirDao implements IDao, postPersist(theEntity, (T) theResource); + } else if (theEntity.getDeleted() != null) { + theEntity = myEntityManager.merge(theEntity); + + postDelete(theEntity); + } else { theEntity = myEntityManager.merge(theEntity); @@ -2014,10 +2065,6 @@ public abstract class BaseHapiFhirDao implements IDao, */ if (theCreateNewHistoryEntry) { final ResourceHistoryTable historyEntry = theEntity.toHistory(); -// if (theEntity.getVersion() > 1) { -// existing = myResourceHistoryTableDao.findForIdAndVersion(theEntity.getId(), theEntity.getVersion()); -// ourLog.warn("Reusing existing history entry entity {}", theEntity.getIdDt().getValue()); -// } historyEntry.setEncoding(changed.getEncoding()); historyEntry.setResource(changed.getResource()); @@ -2057,6 +2104,7 @@ public abstract class BaseHapiFhirDao implements IDao, if (thePerformIndexing) { for (ResourceIndexedSearchParamString next : removeCommon(existingStringParams, stringParams)) { + next.setDaoConfig(myConfig); myEntityManager.remove(next); theEntity.getParamsString().remove(next); } @@ -2148,12 +2196,11 @@ public abstract class BaseHapiFhirDao implements IDao, } // if thePerformIndexing - theEntity = myEntityManager.merge(theEntity); - if (theResource != null) { populateResourceIdFromEntity(theEntity, theResource); } + return theEntity; } 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 6cff70e1f1e..b8a5df18c3f 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 @@ -50,6 +50,7 @@ import ca.uhn.fhir.rest.server.method.SearchMethodBinding; import ca.uhn.fhir.util.*; import org.apache.commons.lang3.Validate; 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; @@ -207,7 +208,7 @@ public abstract class BaseHapiFhirResourceDao extends B StopWatch w = new StopWatch(); - T resourceToDelete = toResource(myResourceType, entity, false); + T resourceToDelete = toResource(myResourceType, entity, null, false); // Notify IServerOperationInterceptors about pre-action call if (theReques != null) { @@ -289,7 +290,7 @@ public abstract class BaseHapiFhirResourceDao extends B ResourceTable entity = myEntityManager.find(ResourceTable.class, pid); deletedResources.add(entity); - T resourceToDelete = toResource(myResourceType, entity, false); + T resourceToDelete = toResource(myResourceType, entity, null, false); // Notify IServerOperationInterceptors about pre-action call if (theRequest != null) { @@ -394,16 +395,6 @@ public abstract class BaseHapiFhirResourceDao extends B "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"); } createForcedIdIfNeeded(entity, theResource.getIdElement()); - - if (entity.getForcedId() != null) { - try { - translateForcedIdToPid(getResourceName(), theResource.getIdElement().getIdPart()); - throw new UnprocessableEntityException(getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "duplicateCreateForcedId", theResource.getIdElement().getIdPart())); - } catch (ResourceNotFoundException e) { - // good, this ID doesn't exist so we can create it - } - } - } // Notify interceptors @@ -517,6 +508,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional(propagation = Propagation.NEVER) public ExpungeOutcome expunge(IIdType theId, ExpungeOptions theExpungeOptions) { BaseHasResource entity = readEntity(theId); if (theId.hasVersionIdPart()) { @@ -532,6 +524,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override + @Transactional(propagation = Propagation.NEVER) public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions) { ourLog.info("Beginning TYPE[{}] expunge operation", getResourceName()); @@ -854,16 +847,10 @@ public abstract class BaseHapiFhirResourceDao extends B BaseHasResource entity = readEntity(theId); validateResourceType(entity); - T retVal = toResource(myResourceType, entity, false); + T retVal = toResource(myResourceType, entity, null, false); - IPrimitiveType deleted; - if (retVal instanceof IResource) { - deleted = ResourceMetadataKeyEnum.DELETED_AT.get((IResource) retVal); - } else { - deleted = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) retVal); - } - if (deleted != null && !deleted.isEmpty()) { - throw new ResourceGoneException("Resource was deleted at " + deleted.getValueAsString()); + 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()); @@ -930,10 +917,14 @@ public abstract class BaseHapiFhirResourceDao extends B @Override public void reindex(T theResource, ResourceTable theEntity) { - ourLog.debug("Indexing resource {} - PID {}", theResource.getIdElement().getValue(), theEntity.getId()); - CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE); - updateEntity(null, theResource, theEntity, null, true, false, theEntity.getUpdatedDate(), true, false); - CURRENTLY_REINDEXING.put(theResource, null); + ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getId()); + if (theResource != null) { + CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE); + } + updateEntity(null, theResource, theEntity, theEntity.getDeleted(), true, false, theEntity.getUpdatedDate(), true, false); + if (theResource != null) { + CURRENTLY_REINDEXING.put(theResource, null); + } } @Override @@ -1065,6 +1056,11 @@ public abstract class BaseHapiFhirResourceDao extends B mySecondaryPrimaryKeyParamName = theSecondaryPrimaryKeyParamName; } + @PostConstruct + public void start() { + ourLog.debug("Starting resource DAO for type: {}", getResourceName()); + } + protected MT toMetaDt(Class theType, Collection tagDefinitions) { MT retVal; try { @@ -1205,7 +1201,9 @@ public abstract class BaseHapiFhirResourceDao extends B } } else { /* - * Note: resourcdeId will not be null or empty here, because we check it and reject requests in BaseOutcomeReturningMethodBindingWithResourceParam + * Note: resourceId will not be null or empty here, because we + * check it and reject requests in + * BaseOutcomeReturningMethodBindingWithResourceParam */ resourceId = theResource.getIdElement(); @@ -1336,9 +1334,4 @@ public abstract class BaseHapiFhirResourceDao extends B } } - @PostConstruct - public void start() { - ourLog.info("Starting resource DAO for type: {}", getResourceName()); - } - } 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 58d9d74ad1c..25639f637ec 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 @@ -257,7 +257,7 @@ public abstract class BaseHapiFhirSystemDao extends BaseHapiFhirDao extends BaseHapiFhirDao resourceClass = getContext().getResourceDefinition(resourceTable.getResourceType()).getImplementingClass(); + @SuppressWarnings("rawtypes") final IFhirResourceDao dao = getDaoOrThrowException(resourceClass); dao.reindex(resource, resourceTable); return null; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamExtractor.java index 62fb3442679..258b5bf291a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamExtractor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseSearchParamExtractor.java @@ -20,45 +20,43 @@ package ca.uhn.fhir.jpa.dao; * #L% */ -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.regex.Pattern; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.util.FhirTerser; +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; -import com.google.common.annotations.VisibleForTesting; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.util.FhirTerser; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; public abstract class BaseSearchParamExtractor implements ISearchParamExtractor { - - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSearchParamExtractor.class); - public static final Pattern SPLIT = Pattern.compile("\\||( or )"); + public static final Pattern SPLIT = Pattern.compile("\\||( or )"); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSearchParamExtractor.class); @Autowired private FhirContext myContext; - + @Autowired + private DaoConfig myDaoConfig; @Autowired private ISearchParamRegistry mySearchParamRegistry; - public BaseSearchParamExtractor() { super(); } - public BaseSearchParamExtractor(FhirContext theCtx, ISearchParamRegistry theSearchParamRegistry) { + public BaseSearchParamExtractor(DaoConfig theDaoConfig, FhirContext theCtx, ISearchParamRegistry theSearchParamRegistry) { myContext = theCtx; mySearchParamRegistry = theSearchParamRegistry; + myDaoConfig = theDaoConfig; } - + @Override public List extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) { List refs = new ArrayList(); @@ -95,20 +93,24 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor } } catch (Exception e) { RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource); - ourLog.warn("Failed to index values from path[{}] in resource type[{}]: {}", new Object[] { nextPathTrimmed, def.getName(), e.toString(), e } ); + ourLog.warn("Failed to index values from path[{}] in resource type[{}]: {}", new Object[] {nextPathTrimmed, def.getName(), e.toString(), e}); } } return values; } - + protected FhirContext getContext() { return myContext; } + public DaoConfig getDaoConfig() { + return myDaoConfig; + } + public Collection getSearchParams(IBaseResource theResource) { RuntimeResourceDefinition def = getContext().getResourceDefinition(theResource); Collection retVal = mySearchParamRegistry.getActiveSearchParams(def.getName()).values(); - List defaultList= Collections.emptyList(); + List defaultList = Collections.emptyList(); retVal = ObjectUtils.defaultIfNull(retVal, defaultList); return retVal; } 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 d2d3595f6bc..9c6a156cefc 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 @@ -89,7 +89,7 @@ public class DaoConfig { /** * update setter javadoc if default changes */ - private boolean myAllowContainsSearches = true; + private boolean myAllowContainsSearches = false; /** * update setter javadoc if default changes @@ -754,7 +754,15 @@ public class DaoConfig { * If enabled, the server will support the use of :contains searches, * which are helpful but can have adverse effects on performance. *

    - * Default is true + * 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; @@ -764,12 +772,21 @@ public class DaoConfig { * If enabled, the server will support the use of :contains searches, * which are helpful but can have adverse effects on performance. *

    - * Default is true + * 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 diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoMessageHeaderDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoMessageHeaderDstu2.java new file mode 100644 index 00000000000..19c1c627d0e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoMessageHeaderDstu2.java @@ -0,0 +1,38 @@ +package ca.uhn.fhir.jpa.dao; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.model.dstu2.resource.MessageHeader; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; +import org.hl7.fhir.instance.model.api.IBaseBundle; + +public class FhirResourceDaoMessageHeaderDstu2 extends FhirResourceDaoDstu2 implements IFhirResourceDaoMessageHeader { + + @Override + public IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) { + return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented(); + } + + public static IBaseBundle throwProcessMessageNotImplemented() { + throw new NotImplementedOperationException("This operation is not yet implemented on this server"); + } +} 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 fb97650f2f6..b3a87e35165 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 @@ -28,6 +28,7 @@ import java.util.*; import javax.annotation.PostConstruct; import org.apache.commons.codec.binary.StringUtils; +import org.hl7.fhir.instance.hapi.validation.CachingValidationSupport; import org.hl7.fhir.instance.hapi.validation.DefaultProfileValidationSupport; import org.hl7.fhir.instance.hapi.validation.ValidationSupportChain; import org.hl7.fhir.instance.model.api.IIdType; @@ -62,7 +63,7 @@ public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2 @Qualifier("myFhirContextDstu2Hl7Org") private FhirContext myRiCtx; - private ValidationSupportChain myValidationSupport; + private CachingValidationSupport myValidationSupport; private void addCompose(String theFilter, ValueSet theValueSetToPopulate, ValueSet theSourceValueSet, CodeSystemConcept theConcept) { if (isBlank(theFilter)) { @@ -252,7 +253,7 @@ public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2 public void postConstruct() { super.postConstruct(); myDefaultProfileValidationSupport = new DefaultProfileValidationSupport(); - myValidationSupport = new ValidationSupportChain(myDefaultProfileValidationSupport, myJpaValidationSupport); + myValidationSupport = new CachingValidationSupport(new ValidationSupportChain(myDefaultProfileValidationSupport, myJpaValidationSupport)); } @Override 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 90f8e6e4a15..fd47335fb25 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,15 +1,18 @@ package ca.uhn.fhir.jpa.dao; +import java.util.Collection; +import java.util.Set; + +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 org.hl7.fhir.instance.model.api.IBaseResource; - -import java.util.Collection; -import java.util.Set; /* * #%L @@ -56,6 +59,6 @@ public interface IDao { IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation); - R toResource(Class theResourceType, BaseHasResource theEntity, boolean theForHistoryOperation); + R toResource(Class theResourceType, IBaseResourceEntity theEntity, Collection theTagList, boolean theForHistoryOperation); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoMessageHeader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoMessageHeader.java new file mode 100644 index 00000000000..1bbc494b96c --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFhirResourceDaoMessageHeader.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.jpa.dao; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; + +/* + * #%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 IFhirResourceDaoMessageHeader extends IFhirResourceDao { + + IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage); + +} 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 bc33de028e1..2d76bb864d8 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 @@ -23,6 +23,8 @@ 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.IResourceIndexedSearchParamUriDao; +import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao; +import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; @@ -53,7 +55,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; @@ -61,6 +62,8 @@ import org.apache.commons.lang3.tuple.Pair; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.query.Query; +import org.hibernate.query.criteria.internal.CriteriaBuilderImpl; +import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPredicate; import org.hl7.fhir.dstu3.model.BaseResource; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -69,7 +72,6 @@ import org.hl7.fhir.instance.model.api.IIdType; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import javax.persistence.criteria.*; -import javax.persistence.criteria.CriteriaBuilder.In; import java.math.BigDecimal; import java.math.MathContext; import java.util.*; @@ -108,12 +110,17 @@ public class SearchBuilder implements ISearchBuilder { private IHapiTerminologySvc myTerminologySvc; private int myFetchSize; + protected IResourceTagDao myResourceTagDao; + protected IResourceSearchViewDao myResourceSearchViewDao; + /** * Constructor */ - public SearchBuilder(FhirContext theFhirContext, EntityManager theEntityManager, IFulltextSearchSvc theFulltextSearchSvc, - BaseHapiFhirDao theDao, - IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao, IHapiTerminologySvc theTerminologySvc, ISearchParamRegistry theSearchParamRegistry) { + public 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; @@ -122,6 +129,8 @@ public class SearchBuilder implements ISearchBuilder { myForcedIdDao = theForcedIdDao; myTerminologySvc = theTerminologySvc; mySearchParamRegistry = theSearchParamRegistry; + myResourceTagDao = theResourceTagDao; + myResourceSearchViewDao = theResourceViewDao; } private void addPredicateComposite(String theResourceName, RuntimeSearchParam theParamDef, List theNextAnd) { @@ -257,7 +266,7 @@ public class SearchBuilder implements ISearchBuilder { return; } - List codePredicates = new ArrayList(); + List codePredicates = new ArrayList<>(); for (IQueryParameterType nextOr : theList) { IQueryParameterType params = nextOr; @@ -273,8 +282,9 @@ public class SearchBuilder implements ISearchBuilder { ParamPrefixEnum prefix = ObjectUtils.defaultIfNull(param.getPrefix(), ParamPrefixEnum.EQUAL); String invalidMessageName = "invalidNumberPrefix"; - Predicate num = createPredicateNumeric(theResourceName, theParamName, join, myBuilder, params, prefix, value, fromObj, invalidMessageName); - codePredicates.add(num); + Predicate predicateNumeric = createPredicateNumeric(theResourceName, theParamName, join, myBuilder, params, prefix, value, fromObj, invalidMessageName); + Predicate predicateOuter = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, predicateNumeric ); + codePredicates.add(predicateOuter); } else { throw new IllegalArgumentException("Invalid token type: " + params.getClass()); @@ -287,11 +297,10 @@ public class SearchBuilder implements ISearchBuilder { private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) { Join paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT); - Join paramJoin = paramPresentJoin.join("mySearchParam", JoinType.LEFT); - myPredicates.add(myBuilder.equal(paramJoin.get("myResourceName"), theResourceName)); - myPredicates.add(myBuilder.equal(paramJoin.get("myParamName"), theParamName)); - myPredicates.add(myBuilder.equal(paramPresentJoin.get("myPresent"), !theMissing)); + Expression hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class); + Long hash = SearchParamPresent.calculateHashPresence(theResourceName, theParamName, !theMissing); + myPredicates.add(myBuilder.equal(hashPresence, hash)); } private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing, Join theJoin) { @@ -309,7 +318,7 @@ public class SearchBuilder implements ISearchBuilder { return; } - List codePredicates = new ArrayList(); + List codePredicates = new ArrayList<>(); for (IQueryParameterType nextOr : theList) { Predicate singleCode = createPredicateQuantity(nextOr, theResourceName, theParamName, myBuilder, join); @@ -332,7 +341,7 @@ public class SearchBuilder implements ISearchBuilder { Join join = createOrReuseJoin(JoinEnum.REFERENCE, theParamName); - List codePredicates = new ArrayList(); + List codePredicates = new ArrayList<>(); for (IQueryParameterType nextOr : theList) { @@ -429,7 +438,7 @@ public class SearchBuilder implements ISearchBuilder { } else { RuntimeResourceDefinition resDef = myContext.getResourceDefinition(ref.getResourceType()); - resourceTypes = new ArrayList>(1); + resourceTypes = new ArrayList<>(1); resourceTypes.add(resDef.getImplementingClass()); resourceId = ref.getIdPart(); } @@ -474,7 +483,7 @@ public class SearchBuilder implements ISearchBuilder { IQueryParameterType chainValue; if (remainingChain != null) { if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { - ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", new Object[] {nextType.getSimpleName(), chain, remainingChain}); + ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", nextType.getSimpleName(), chain, remainingChain); continue; } @@ -495,7 +504,7 @@ public class SearchBuilder implements ISearchBuilder { Root subQfrom = subQ.from(ResourceTable.class); subQ.select(subQfrom.get("myId").as(Long.class)); - List> andOrParams = new ArrayList>(); + List> andOrParams = new ArrayList<>(); andOrParams.add(Collections.singletonList(chainValue)); /* @@ -546,7 +555,7 @@ public class SearchBuilder implements ISearchBuilder { private void addPredicateResourceId(List> theValues) { for (List nextValue : theValues) { - Set orPids = new HashSet(); + Set orPids = new HashSet<>(); for (IQueryParameterType next : nextValue) { String value = next.getValueAsQueryToken(myContext); if (value != null && value.startsWith("|")) { @@ -594,10 +603,9 @@ public class SearchBuilder implements ISearchBuilder { return; } - List codePredicates = new ArrayList(); + List codePredicates = new ArrayList<>(); for (IQueryParameterType nextOr : theList) { - IQueryParameterType theParameter = nextOr; - Predicate singleCode = createPredicateString(theParameter, theResourceName, theParamName, myBuilder, join); + Predicate singleCode = createPredicateString(nextOr, theResourceName, theParamName, myBuilder, join); codePredicates.add(singleCode); } @@ -742,7 +750,7 @@ public class SearchBuilder implements ISearchBuilder { return; } - List codePredicates = new ArrayList(); + List codePredicates = new ArrayList<>(); for (IQueryParameterType nextOr : theList) { if (nextOr instanceof TokenParam) { @@ -785,7 +793,6 @@ public class SearchBuilder implements ISearchBuilder { continue; } - Predicate predicate; if (param.getQualifier() == UriParamQualifierEnum.ABOVE) { /* @@ -814,14 +821,24 @@ public class SearchBuilder implements ISearchBuilder { continue; } - predicate = join.get("myUri").as(String.class).in(toFind); + Predicate uriPredicate = join.get("myUri").as(String.class).in(toFind); + Predicate hashAndUriPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, uriPredicate); + codePredicates.add(hashAndUriPredicate); } else if (param.getQualifier() == UriParamQualifierEnum.BELOW) { - predicate = myBuilder.like(join.get("myUri").as(String.class), createLeftMatchLikeExpression(value)); + + Predicate uriPredicate = myBuilder.like(join.get("myUri").as(String.class), createLeftMatchLikeExpression(value)); + Predicate hashAndUriPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, uriPredicate); + codePredicates.add(hashAndUriPredicate); + } else { - predicate = myBuilder.equal(join.get("myUri").as(String.class), value); + + long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(theResourceName, theParamName, value); + Predicate hashPredicate = myBuilder.equal(join.get("myHashUri"), hashUri); + codePredicates.add(hashPredicate); + } - codePredicates.add(predicate); + } else { throw new IllegalArgumentException("Invalid URI type: " + nextOr.getClass()); } @@ -839,16 +856,13 @@ public class SearchBuilder implements ISearchBuilder { } Predicate orPredicate = myBuilder.or(toArray(codePredicates)); - - Predicate outerPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, orPredicate); - myPredicates.add(outerPredicate); + myPredicates.add(orPredicate); } private Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From theFrom, Predicate thePredicate) { - Predicate resourceTypePredicate = myBuilder.equal(theFrom.get("myResourceType"), theResourceName); - Predicate paramNamePredicate = myBuilder.equal(theFrom.get("myParamName"), theParamName); - Predicate outerPredicate = myBuilder.and(resourceTypePredicate, paramNamePredicate, thePredicate); - return outerPredicate; + long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); + Predicate hashIdentityPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity); + return myBuilder.and(hashIdentityPredicate, thePredicate); } private Predicate createCompositeParamPart(String theResourceName, Root theRoot, RuntimeSearchParam theParam, IQueryParameterType leftValue) { @@ -1028,7 +1042,7 @@ public class SearchBuilder implements ISearchBuilder { if (theParamName == null) { return num; } - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, num); + return num; } private Predicate createPredicateQuantity(IQueryParameterType theParam, String theResourceName, String theParamName, CriteriaBuilder theBuilder, @@ -1054,39 +1068,31 @@ public class SearchBuilder implements ISearchBuilder { throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass()); } - Predicate system = null; - if (!isBlank(systemValue)) { - system = theBuilder.equal(theFrom.get("mySystem"), systemValue); - } - - Predicate code = null; - if (!isBlank(unitsValue)) { - code = theBuilder.equal(theFrom.get("myUnits"), unitsValue); + Predicate hashPredicate; + if (!isBlank(systemValue) && !isBlank(unitsValue)) { + long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(theResourceName, theParamName, systemValue, unitsValue); + hashPredicate = myBuilder.equal(theFrom.get("myHashIdentitySystemAndUnits"), hash); + } else if (!isBlank(unitsValue)) { + long hash = ResourceIndexedSearchParamQuantity.calculateHashUnits(theResourceName, theParamName, unitsValue); + hashPredicate = myBuilder.equal(theFrom.get("myHashIdentityAndUnits"), hash); + } else { + long hash = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); + hashPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hash); } cmpValue = ObjectUtils.defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL); final Expression path = theFrom.get("myValue"); String invalidMessageName = "invalidQuantityPrefix"; - Predicate num = createPredicateNumeric(theResourceName, null, theFrom, theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName); + Predicate numericPredicate = createPredicateNumeric(theResourceName, null, theFrom, theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName); - Predicate singleCode; - if (system == null && code == null) { - singleCode = num; - } else if (system == null) { - singleCode = theBuilder.and(code, num); - } else if (code == null) { - singleCode = theBuilder.and(system, num); - } else { - singleCode = theBuilder.and(system, code, num); - } - - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + return theBuilder.and(hashPredicate, numericPredicate); } 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()) { @@ -1097,7 +1103,7 @@ public class SearchBuilder implements ISearchBuilder { StringParam id = (StringParam) theParameter; rawSearchTerm = id.getValue(); if (id.isContains()) { - if (!myCallingDao.getConfig().isAllowContainsSearches()) { + if (!daoConfig.isAllowContainsSearches()) { throw new MethodNotAllowedException(":contains modifier is disabled on this server"); } } @@ -1113,22 +1119,34 @@ public class SearchBuilder implements ISearchBuilder { + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm); } - String likeExpression = BaseHapiFhirDao.normalizeString(rawSearchTerm); - if (theParameter instanceof StringParam && - ((StringParam) theParameter).isContains() && - myCallingDao.getConfig().isAllowContainsSearches()) { - likeExpression = createLeftAndRightMatchLikeExpression(likeExpression); + boolean exactMatch = theParameter instanceof StringParam && ((StringParam) theParameter).isExact(); + if (exactMatch) { + + // Exact match + + Long hash = ResourceIndexedSearchParamString.calculateHashExact(theResourceName, theParamName, rawSearchTerm); + return theBuilder.equal(theFrom.get("myHashExact").as(Long.class), hash); + } else { - likeExpression = createLeftMatchLikeExpression(likeExpression); - } - Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); - if (theParameter instanceof StringParam && ((StringParam) theParameter).isExact()) { - Predicate exactCode = theBuilder.equal(theFrom.get("myValueExact"), rawSearchTerm); - singleCode = theBuilder.and(singleCode, exactCode); - } + // Normalized Match - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + String normalizedString = BaseHapiFhirDao.normalizeString(rawSearchTerm); + String likeExpression; + if (theParameter instanceof StringParam && + ((StringParam) theParameter).isContains() && + daoConfig.isAllowContainsSearches()) { + likeExpression = createLeftAndRightMatchLikeExpression(normalizedString); + } else { + likeExpression = createLeftMatchLikeExpression(normalizedString); + } + + Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(daoConfig, 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); + + } } private List createPredicateTagList(Path theDefJoin, CriteriaBuilder theBuilder, TagTypeEnum theTagType, List> theTokens) { @@ -1183,7 +1201,7 @@ public class SearchBuilder implements ISearchBuilder { * Process token modifiers (:in, :below, :above) */ - List codes = null; + List codes; if (modifier == TokenParamModifier.IN) { codes = myTerminologySvc.expandValueSet(code); } else if (modifier == TokenParamModifier.ABOVE) { @@ -1192,81 +1210,53 @@ public class SearchBuilder implements ISearchBuilder { } else if (modifier == TokenParamModifier.BELOW) { system = determineSystemIfMissing(theParamName, code, system); codes = myTerminologySvc.findCodesBelow(system, code); - } - - ArrayList singleCodePredicates = new ArrayList<>(); - if (codes != null) { - - if (codes.isEmpty()) { - - // This will never match anything - Predicate codePredicate = theBuilder.isNull(theFrom.get("myMissing")); - singleCodePredicates.add(codePredicate); - - } else { - List orPredicates = new ArrayList(); - Map> map = new HashMap>(); - for (VersionIndependentConcept nextCode : codes) { - List systemCodes = map.get(nextCode.getSystem()); - if (null == systemCodes) { - systemCodes = new ArrayList<>(); - map.put(nextCode.getSystem(), systemCodes); - } - systemCodes.add(nextCode); - } - // Use "in" in case of large numbers of codes due to param modifiers - final Path systemExpression = theFrom.get("mySystem"); - final Path valueExpression = theFrom.get("myValue"); - for (Map.Entry> entry : map.entrySet()) { - Predicate systemPredicate = theBuilder.equal(systemExpression, entry.getKey()); - In codePredicate = theBuilder.in(valueExpression); - for (VersionIndependentConcept nextCode : entry.getValue()) { - codePredicate.value(nextCode.getCode()); - } - orPredicates.add(theBuilder.and(systemPredicate, codePredicate)); - } - - singleCodePredicates.add(theBuilder.or(orPredicates.toArray(new Predicate[orPredicates.size()]))); - } - } else { + codes = Collections.singletonList(new VersionIndependentConcept(system, code)); + } - /* - * Ok, this is a normal query - */ + if (codes.isEmpty()) { + // This will never match anything + return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false); + } - if (StringUtils.isNotBlank(system)) { - if (modifier != null && modifier == TokenParamModifier.NOT) { - singleCodePredicates.add(theBuilder.notEqual(theFrom.get("mySystem"), system)); - } else { - singleCodePredicates.add(theBuilder.equal(theFrom.get("mySystem"), system)); - } - } else if (system == null) { - // don't check the system + /* + * Note: A null system value means "match any system", but + * an empty-string system value means "match values that + * explicitly have no system". + */ + boolean haveSystem = codes.get(0).getSystem() != null; + boolean haveCode = isNotBlank(codes.get(0).getCode()); + Expression hashField; + if (!haveSystem && !haveCode) { + // If we have neither, this isn't actually an expression so + // just return 1=1 + return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, true); + } else if (haveSystem && haveCode) { + hashField = theFrom.get("myHashSystemAndValue").as(Long.class); + } else if (haveSystem) { + hashField = theFrom.get("myHashSystem").as(Long.class); + } else { + hashField = theFrom.get("myHashValue").as(Long.class); + } + + List values = new ArrayList<>(codes.size()); + for (VersionIndependentConcept next : codes) { + if (haveSystem && haveCode) { + values.add(ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, next.getSystem(), next.getCode())); + } else if (haveSystem) { + values.add(ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, next.getSystem())); } else { - // If the system is "", we only match on null systems - singleCodePredicates.add(theBuilder.isNull(theFrom.get("mySystem"))); - } - - if (StringUtils.isNotBlank(code)) { - if (modifier != null && modifier == TokenParamModifier.NOT) { - singleCodePredicates.add(theBuilder.notEqual(theFrom.get("myValue"), code)); - } else { - singleCodePredicates.add(theBuilder.equal(theFrom.get("myValue"), code)); - } - } else { - /* - * As of HAPI FHIR 1.5, if the client searched for a token with a system but no specified value this means to - * match all tokens with the given value. - * - * I'm not sure I agree with this, but hey.. FHIR-I voted and this was the result :) - */ - // singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue"))); + values.add(ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, next.getCode())); } } - Predicate singleCode = theBuilder.and(toArray(singleCodePredicates)); - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + Predicate predicate = hashField.in(values); + if (modifier == TokenParamModifier.NOT) { + Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName)); + Predicate disjunctionPredicate = theBuilder.not(predicate); + predicate = theBuilder.and(identityPredicate, disjunctionPredicate); + } + return predicate; } @Override @@ -1371,8 +1361,8 @@ public class SearchBuilder implements ISearchBuilder { if (myParams.getEverythingMode() != null) { Join join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT); - if (myParams.get(BaseResource.SP_RES_ID) != null) { - StringParam idParm = (StringParam) myParams.get(BaseResource.SP_RES_ID).get(0).get(0); + 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); if (myAlsoIncludePids == null) { myAlsoIncludePids = new ArrayList<>(1); @@ -1462,7 +1452,7 @@ public class SearchBuilder implements ISearchBuilder { return false; } - if (BaseResource.SP_RES_ID.equals(theSort.getParamName())) { + if (IAnyResource.SP_RES_ID.equals(theSort.getParamName())) { From forcedIdJoin = theFrom.join("myForcedId", JoinType.LEFT); if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) { theOrders.add(theBuilder.asc(forcedIdJoin.get("myForcedId"))); @@ -1602,35 +1592,39 @@ public class SearchBuilder implements ISearchBuilder { private void doLoadPids(List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation, EntityManager entityManager, FhirContext context, IDao theDao, Map position, Collection pids) { - CriteriaBuilder builder = entityManager.getCriteriaBuilder(); - CriteriaQuery cq = builder.createQuery(ResourceTable.class); - Root from = cq.from(ResourceTable.class); - cq.where(from.get("myId").in(pids)); - TypedQuery q = entityManager.createQuery(cq); - List resultList = q.getResultList(); - - for (ResourceTable next : resultList) { + // -- get the resource from the searchView + Collection resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(pids); + + //-- preload all tags with tag definition if any + Map> tagMap = getResourceTagMap(resourceSearchViewList); + + Long resourceId = null; + for (ResourceSearchView next : resourceSearchViewList) { + Class resourceType = context.getResourceDefinition(next.getResourceType()).getImplementingClass(); - IBaseResource resource = theDao.toResource(resourceType, next, theForHistoryOperation); + + resourceId = next.getId(); + + IBaseResource resource = theDao.toResource(resourceType, next, tagMap.get(resourceId), theForHistoryOperation); if (resource == null) { ourLog.warn("Unable to find resource {}/{}/_history/{} in database", next.getResourceType(), next.getIdDt().getIdPart(), next.getVersion()); continue; } - Integer index = position.get(next.getId()); + Integer index = position.get(resourceId); if (index == null) { - ourLog.warn("Got back unexpected resource PID {}", next.getId()); + ourLog.warn("Got back unexpected resource PID {}", resourceId); continue; } if (resource instanceof IResource) { - if (theRevIncludedPids.contains(next.getId())) { + if (theRevIncludedPids.contains(resourceId)) { ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.INCLUDE); } else { ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.MATCH); } } else { - if (theRevIncludedPids.contains(next.getId())) { + if (theRevIncludedPids.contains(resourceId)) { ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.INCLUDE.getCode()); } else { ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.MATCH.getCode()); @@ -1641,6 +1635,44 @@ public class SearchBuilder implements ISearchBuilder { } } + private Map> getResourceTagMap(Collection theResourceSearchViewList) { + + List idList = new ArrayList(theResourceSearchViewList.size()); + + //-- find all resource has tags + for (ResourceSearchView resource: theResourceSearchViewList) { + if (resource.isHasTags()) + idList.add(resource.getId()); + } + + Map> tagMap = new HashMap<>(); + + //-- no tags + if (idList.size() == 0) + return tagMap; + + //-- get all tags for the idList + Collection tagList = myResourceTagDao.findByResourceIds(idList); + + //-- build the map, key = resourceId, value = list of ResourceTag + Long resourceId; + Collection tagCol; + for (ResourceTag tag : tagList) { + + resourceId = tag.getResourceId(); + tagCol = tagMap.get(resourceId); + if (tagCol == null) { + tagCol = new ArrayList<>(); + tagCol.add(tag); + tagMap.put(resourceId, tagCol); + } else { + tagCol.add(tag); + } + } + + return tagMap; + } + @Override public void loadResourcesByPid(Collection theIncludePids, List theResourceListToPopulate, Set theRevIncludedPids, boolean theForHistoryOperation, EntityManager entityManager, FhirContext context, IDao theDao) { @@ -1677,18 +1709,16 @@ public class SearchBuilder implements ISearchBuilder { } /** - * THIS SHOULD RETURN HASHSET and not jsut Set because we add to it later (so it can't be Collections.emptySet()) - * - * @param theLastUpdated + * THIS SHOULD RETURN HASHSET and not just Set because we add to it later (so it can't be Collections.emptySet()) */ @Override public HashSet loadReverseIncludes(IDao theCallingDao, FhirContext theContext, EntityManager theEntityManager, Collection theMatches, Set theRevIncludes, boolean theReverseMode, DateRangeParam theLastUpdated) { if (theMatches.size() == 0) { - return new HashSet(); + return new HashSet<>(); } if (theRevIncludes == null || theRevIncludes.isEmpty()) { - return new HashSet(); + return new HashSet<>(); } String searchFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid"; @@ -1729,7 +1759,7 @@ public class SearchBuilder implements ISearchBuilder { } else { List paths; - RuntimeSearchParam param = null; + RuntimeSearchParam param; String resType = nextInclude.getParamType(); if (isBlank(resType)) { continue; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java index 5fe006ecee1..0352a99f074 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParamExtractorDstu2.java @@ -59,7 +59,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen searchTerm = searchTerm.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -68,7 +68,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen if (value.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { value = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value); nextEntity.setResource(theEntity); retVal.add(nextEntity); } 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 2d24d6c7b00..9e2dc7c63e0 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 @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.dao.data; +import java.util.Collection; import java.util.List; /* @@ -38,5 +39,7 @@ public interface IForcedIdDao extends JpaRepository { @Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid") public ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid); - + + @Query("SELECT f FROM ForcedId f WHERE f.myResourcePid in (:pids)") + Collection findByResourcePids(@Param("pids") Collection pids); } 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 d6250ce64f6..03fa39d7957 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 @@ -1,6 +1,10 @@ package ca.uhn.fhir.jpa.dao.data; -import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; +import java.util.Collection; +import java.util.Date; + +import javax.persistence.TemporalType; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -8,8 +12,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Temporal; import org.springframework.data.repository.query.Param; -import javax.persistence.TemporalType; -import java.util.Date; +import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; /* * #%L @@ -82,4 +85,10 @@ public interface IResourceHistoryTableDao extends JpaRepository findIdsOfPreviousVersionsOfResources(Pageable thePage); + + @Query("" + + "SELECT h FROM ResourceHistoryTable h " + + "INNER JOIN ResourceTable r ON (r.myId = h.myResourceId and r.myVersion = h.myResourceVersion) " + + "WHERE r.myId in (:pids)") + Collection findByResourceIds(@Param("pids") Collection pids); } 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 6bd5724bc8a..7b1ae15b3e7 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 @@ -23,7 +23,12 @@ package ca.uhn.fhir.jpa.dao.data; import org.springframework.data.jpa.repository.JpaRepository; import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface IResourceIndexedSearchParamStringDao extends JpaRepository { - // nothing yet + + @Query("select count(*) from ResourceIndexedSearchParamString t WHERE t.myResourcePid = :resid") + int countForResourceId(@Param("resid") Long theResourcePid); + } 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 09679d390b0..9e30fd3b026 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,10 +20,14 @@ package ca.uhn.fhir.jpa.dao.data; * #L% */ -import org.springframework.data.jpa.repository.JpaRepository; - import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface IResourceIndexedSearchParamTokenDao extends JpaRepository { - // nothing yet + + @Query("select count(*) from ResourceIndexedSearchParamToken t WHERE t.myResourcePid = :resid") + int countForResourceId(@Param("resid") Long theResourcePid); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceSearchViewDao.java similarity index 68% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamDao.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceSearchViewDao.java index e2184820d55..cf13d923fc6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceSearchViewDao.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.jpa.dao.data; +import java.util.Collection; + /* * #%L * HAPI FHIR JPA Server @@ -10,7 +12,7 @@ package ca.uhn.fhir.jpa.dao.data; * 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 + * 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, @@ -24,11 +26,10 @@ 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.SearchParam; +import ca.uhn.fhir.jpa.entity.ResourceSearchView; -public interface ISearchParamDao extends JpaRepository { - - @Query("SELECT s FROM SearchParam s WHERE s.myResourceName = :resname AND s.myParamName = :parmname") - public SearchParam findForResource(@Param("resname") String theResourceType, @Param("parmname") String theParamName); +public interface IResourceSearchViewDao extends JpaRepository { + @Query("SELECT v FROM ResourceSearchView v WHERE v.myResourceId in (:pids)") + Collection findByResourceIds(@Param("pids") Collection pids); } 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 3acba7469a6..1ba407c1f4f 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 @@ -1,5 +1,7 @@ package ca.uhn.fhir.jpa.dao.data; +import java.util.Collection; + /* * #%L * HAPI FHIR JPA Server @@ -21,9 +23,15 @@ package ca.uhn.fhir.jpa.dao.data; */ 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; public interface IResourceTagDao extends JpaRepository { - // nothing + @Query("" + + "SELECT t FROM ResourceTag t " + + "INNER JOIN TagDefinition td ON (td.myId = t.myTagId) " + + "WHERE t.myResourceId in (:pids)") + Collection findByResourceIds(@Param("pids") Collection pids); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchDao.java index f89d260f512..5414d0fcee4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchDao.java @@ -36,19 +36,19 @@ import ca.uhn.fhir.jpa.entity.Search; public interface ISearchDao extends JpaRepository { @Query("SELECT s FROM Search s WHERE s.myUuid = :uuid") - public Search findByUuid(@Param("uuid") String theUuid); + Search findByUuid(@Param("uuid") String theUuid); @Query("SELECT s.myId FROM Search s WHERE s.mySearchLastReturned < :cutoff") - public Slice findWhereLastReturnedBefore(@Param("cutoff") Date theCutoff, Pageable thePage); + Slice findWhereLastReturnedBefore(@Param("cutoff") Date theCutoff, Pageable thePage); // @Query("SELECT s FROM Search s WHERE s.myCreated < :cutoff") // public Collection findWhereCreatedBefore(@Param("cutoff") Date theCutoff); @Query("SELECT s FROM Search s WHERE s.myResourceType = :type AND mySearchQueryStringHash = :hash AND s.myCreated > :cutoff") - public Collection find(@Param("type") String theResourceType, @Param("hash") int theHashCode, @Param("cutoff") Date theCreatedCutoff); + Collection find(@Param("type") String theResourceType, @Param("hash") int theHashCode, @Param("cutoff") Date theCreatedCutoff); @Modifying @Query("UPDATE Search s SET s.mySearchLastReturned = :last WHERE s.myId = :pid") - public void updateSearchLastReturned(@Param("pid") long thePid, @Param("last") Date theDate); + void updateSearchLastReturned(@Param("pid") long thePid, @Param("last") Date theDate); } 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 12649857f48..4f7e05d9131 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 @@ -38,8 +38,8 @@ public interface ISearchResultDao extends JpaRepository { @Query(value="SELECT r FROM SearchResult r WHERE r.mySearch = :search") Collection findWithSearchUuid(@Param("search") Search theSearch); - @Query(value="SELECT r FROM SearchResult r WHERE r.mySearch = :search ORDER BY r.myOrder ASC") - Page findWithSearchUuid(@Param("search") Search theSearch, Pageable thePage); + @Query(value="SELECT r.myResourcePid FROM SearchResult r WHERE r.mySearch = :search ORDER BY r.myOrder ASC") + Page findWithSearchUuid(@Param("search") Search theSearch, Pageable thePage); @Modifying @Query(value="DELETE FROM SearchResult r WHERE r.mySearchPid = :search") diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermCodeSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermCodeSystemDao.java index fd09ba65cef..910cc556ef6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermCodeSystemDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermCodeSystemDao.java @@ -26,6 +26,8 @@ import org.springframework.data.repository.query.Param; import ca.uhn.fhir.jpa.entity.TermCodeSystem; +import java.util.Optional; + public interface ITermCodeSystemDao extends JpaRepository { @Query("SELECT cs FROM TermCodeSystem cs WHERE cs.myCodeSystemUri = :code_system_uri") @@ -34,4 +36,7 @@ public interface ITermCodeSystemDao extends JpaRepository @Query("SELECT cs FROM TermCodeSystem cs WHERE cs.myResourcePid = :resource_pid") TermCodeSystem findByResourcePid(@Param("resource_pid") Long theReourcePid); + @Query("SELECT cs FROM TermCodeSystem cs WHERE cs.myCurrentVersion.myId = :csv_pid") + Optional findWithCodeSystemVersionAsCurrentVersion(@Param("csv_pid") Long theCodeSystemVersionPid); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java index 5f9dc078b6f..d0f519b2729 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java @@ -1,9 +1,16 @@ package ca.uhn.fhir.jpa.dao.data; -import java.util.List; - +import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; +import ca.uhn.fhir.jpa.entity.TermConcept; import org.springframework.data.domain.Page; 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.repository.query.Param; + +import java.util.List; /* * #%L @@ -14,9 +21,9 @@ import org.springframework.data.domain.Pageable; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,31 +32,25 @@ import org.springframework.data.domain.Pageable; * #L% */ -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 ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; -import ca.uhn.fhir.jpa.entity.TermConcept; - public interface ITermConceptDao extends JpaRepository { + @Query("SELECT COUNT(t) FROM TermConcept t WHERE t.myCodeSystem.myId = :cs_pid") + Integer countByCodeSystemVersion(@Param("cs_pid") Long thePid); + @Query("SELECT c FROM TermConcept c WHERE c.myCodeSystem = :code_system AND c.myCode = :code") TermConcept findByCodeSystemAndCode(@Param("code_system") TermCodeSystemVersion theCodeSystem, @Param("code") String theCode); + @Query("SELECT t FROM TermConcept t WHERE t.myCodeSystem.myId = :cs_pid") + Slice findByCodeSystemVersion(Pageable thePage, @Param("cs_pid") Long thePid); + @Query("SELECT c FROM TermConcept c WHERE c.myCodeSystem = :code_system") List findByCodeSystemVersion(@Param("code_system") TermCodeSystemVersion theCodeSystem); - @Query("SELECT t FROM TermConcept t WHERE t.myCodeSystem.myId = :cs_pid") - @Modifying - List findByCodeSystemVersion(@Param("cs_pid") Long thePid); + @Query("SELECT t FROM TermConcept t WHERE t.myIndexStatus = null") + Page findResourcesRequiringReindexing(Pageable thePageRequest); @Query("UPDATE TermConcept t SET t.myIndexStatus = null") @Modifying int markAllForReindexing(); - @Query("SELECT t FROM TermConcept t WHERE t.myIndexStatus = null") - Page findResourcesRequiringReindexing(Pageable thePageRequest); - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDesignationDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDesignationDao.java index 4be607a9390..38c8346b790 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDesignationDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDesignationDao.java @@ -1,7 +1,11 @@ package ca.uhn.fhir.jpa.dao.data; import ca.uhn.fhir.jpa.entity.TermConceptDesignation; +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.Query; +import org.springframework.data.repository.query.Param; /* * #%L @@ -24,5 +28,11 @@ import org.springframework.data.jpa.repository.JpaRepository; */ public interface ITermConceptDesignationDao extends JpaRepository { - // nothing + + @Query("SELECT t FROM TermConceptDesignation t WHERE t.myCodeSystemVersion.myId = :csv_pid") + Slice findByCodeSystemVersion(Pageable thePage, @Param("csv_pid") Long thePid); + + @Query("SELECT COUNT(t) FROM TermConceptDesignation t WHERE t.myCodeSystemVersion.myId = :csv_pid") + Integer countByCodeSystemVersion(@Param("csv_pid") Long thePid); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptParentChildLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptParentChildLinkDao.java index 5f5be6d3716..d3cb23d9896 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptParentChildLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptParentChildLinkDao.java @@ -1,5 +1,12 @@ package ca.uhn.fhir.jpa.dao.data; +import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; +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.Query; +import org.springframework.data.repository.query.Param; + import java.util.Collection; /* @@ -22,20 +29,15 @@ import java.util.Collection; * #L% */ -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 ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; - public interface ITermConceptParentChildLinkDao extends JpaRepository { - @Query("DELETE FROM TermConceptParentChildLink t WHERE t.myCodeSystem.myId = :cs_pid") - @Modifying - void deleteByCodeSystemVersion(@Param("cs_pid") Long thePid); + @Query("SELECT COUNT(t) FROM TermConceptParentChildLink t WHERE t.myCodeSystem.myId = :cs_pid") + Integer countByCodeSystemVersion(@Param("cs_pid") Long thePid); @Query("SELECT t.myParentPid FROM TermConceptParentChildLink t WHERE t.myChildPid = :child_pid") Collection findAllWithChild(@Param("child_pid") Long theConceptPid); - + + @Query("SELECT t FROM TermConceptParentChildLink t WHERE t.myCodeSystem.myId = :cs_pid") + Slice findByCodeSystemVersion(Pageable thePage, @Param("cs_pid") Long thePid); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptPropertyDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptPropertyDao.java index d0906cef88f..37d276e6e1c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptPropertyDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptPropertyDao.java @@ -1,17 +1,12 @@ package ca.uhn.fhir.jpa.dao.data; -import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; -import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermConceptProperty; -import org.springframework.data.domain.Page; 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.repository.query.Param; -import java.util.List; - /* * #%L * HAPI FHIR JPA Server @@ -33,5 +28,10 @@ import java.util.List; */ public interface ITermConceptPropertyDao extends JpaRepository { - // nothing + + @Query("SELECT t FROM TermConceptProperty t WHERE t.myCodeSystemVersion.myId = :cs_pid") + Slice findByCodeSystemVersion(Pageable thePage, @Param("cs_pid") Long thePid); + + @Query("SELECT COUNT(t) FROM TermConceptProperty t WHERE t.myCodeSystemVersion.myId = :cs_pid") + Integer countByCodeSystemVersion(@Param("cs_pid") Long thePid); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoConceptMapDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoConceptMapDstu3.java index 2df99332ab4..cf15aedcb07 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoConceptMapDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoConceptMapDstu3.java @@ -160,13 +160,16 @@ public class FhirResourceDaoConceptMapDstu3 extends FhirResourceDaoDstu3 implements IFhirResourceDaoMessageHeader { + + @Override + public IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) { + return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoValueSetDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoValueSetDstu3.java index 23397616bc0..2caab674724 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoValueSetDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoValueSetDstu3.java @@ -25,6 +25,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.apache.commons.codec.binary.StringUtils; import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext; @@ -35,6 +36,8 @@ import org.hl7.fhir.dstu3.model.ValueSet.*; import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.ValueSetExpansionOutcome; 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.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -223,6 +226,7 @@ public class FhirResourceDaoValueSetDstu3 extends FhirResourceDaoDstu3 if (vs != null) { ValueSet expansion = doExpand(vs); List contains = expansion.getExpansion().getContains(); + ValidateCodeResult result = validateCodeIsInContains(contains, toStringOrNull(theSystem), toStringOrNull(theCode), theCoding, theCodeableConcept); if (result != null) { if (theDisplay != null && isNotBlank(theDisplay.getValue()) && isNotBlank(result.getDisplay())) { @@ -238,6 +242,9 @@ public class FhirResourceDaoValueSetDstu3 extends FhirResourceDaoDstu3 } + + private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoValueSetDstu3.class); + private String toStringOrNull(IPrimitiveType thePrimitive) { return thePrimitive != null ? thePrimitive.getValue() : null; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3.java index ca2ff8df921..f3aa442c7ff 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3.java @@ -26,12 +26,13 @@ import static org.apache.commons.lang3.StringUtils.trim; import java.math.BigDecimal; import java.util.*; +import javax.annotation.PostConstruct; import javax.measure.unit.NonSI; import javax.measure.unit.Unit; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; -import org.hl7.fhir.dstu3.context.IWorkerContext; +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; @@ -58,6 +59,13 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen @Autowired private org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport myValidationSupport; + private HapiWorkerContext myWorkerContext; + + @PostConstruct + public void start() { + myWorkerContext = new HapiWorkerContext(getContext(), myValidationSupport); + } + /** * Constructor */ @@ -65,8 +73,8 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen super(); } - public SearchParamExtractorDstu3(FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { - super(theCtx, theSearchParamRegistry); + public SearchParamExtractorDstu3(DaoConfig theDaoConfig, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { + super(theDaoConfig, theCtx, theSearchParamRegistry); myValidationSupport = theValidationSupport; } @@ -78,7 +86,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen searchTerm = searchTerm.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -87,7 +95,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen if (value.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { value = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -695,8 +703,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen */ @Override protected List extractValues(String thePaths, IBaseResource theResource) { - IWorkerContext worker = new org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext(getContext(), myValidationSupport); - FHIRPathEngine fp = new FHIRPathEngine(worker); + FHIRPathEngine fp = new FHIRPathEngine(myWorkerContext); List values = new ArrayList<>(); try { 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 0f3bd4cdec8..d3a99267ead 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 @@ -38,7 +38,7 @@ 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.hl7.fhir.r4.hapi.ctx.IValidationSupport.CodeValidationResult; -import org.hl7.fhir.r4.hapi.ctx.ValidationSupportChain; +import org.hl7.fhir.r4.hapi.validation.ValidationSupportChain; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; @@ -47,7 +47,6 @@ import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.IdType; import org.springframework.beans.factory.annotation.Autowired; -import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Date; import java.util.List; 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 c9c3d4d54ae..5c0ff79bf03 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 @@ -156,9 +156,12 @@ public class FhirResourceDaoConceptMapR4 extends FhirResourceDaoR4 i boolean theUpdateVersion, Date theUpdateTime, boolean theForceUpdate, boolean theCreateNewHistoryEntry) { ResourceTable retVal = super.updateEntity(theRequestDetails, theResource, theEntity, theDeletedTimestampOrNull, thePerformIndexing, theUpdateVersion, theUpdateTime, theForceUpdate, theCreateNewHistoryEntry); - ConceptMap conceptMap = (ConceptMap) theResource; - - myHapiTerminologySvc.storeTermConceptMapAndChildren(retVal, conceptMap); + if (retVal.getDeleted() == null) { + ConceptMap conceptMap = (ConceptMap) theResource; + myHapiTerminologySvc.storeTermConceptMapAndChildren(retVal, conceptMap); + } else { + myHapiTerminologySvc.deleteConceptMapAndChildren(retVal); + } return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoMessageHeaderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoMessageHeaderR4.java new file mode 100644 index 00000000000..ba8ce1d8ded --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoMessageHeaderR4.java @@ -0,0 +1,36 @@ +package ca.uhn.fhir.jpa.dao.r4; + +/* + * #%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.FhirResourceDaoMessageHeaderDstu2; +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoMessageHeader; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.r4.model.MessageHeader; + +public class FhirResourceDaoMessageHeaderR4 extends FhirResourceDaoR4 implements IFhirResourceDaoMessageHeader { + + @Override + public IBaseBundle messageHeaderProcessMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) { + return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4.java index 8aab08abb97..53c559b446f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4.java @@ -64,8 +64,8 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements super(); } - public SearchParamExtractorR4(FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { - super(theCtx, theSearchParamRegistry); + public SearchParamExtractorR4(DaoConfig theDaoConfig, FhirContext theCtx, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) { + super(theDaoConfig, theCtx, theSearchParamRegistry); myValidationSupport = theValidationSupport; } @@ -77,7 +77,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements searchTerm = searchTerm.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), resourceName, BaseHapiFhirDao.normalizeString(searchTerm), searchTerm); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -86,7 +86,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements if (value.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { value = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH); } - ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value); + ResourceIndexedSearchParamString nextEntity = new ResourceIndexedSearchParamString(getDaoConfig(), nextSpDef.getName(), BaseHapiFhirDao.normalizeString(value), value); nextEntity.setResource(theEntity); retVal.add(nextEntity); } @@ -104,7 +104,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements */ @Override public Set extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) { - HashSet retVal = new HashSet(); + HashSet retVal = new HashSet<>(); Collection searchParams = getSearchParams(theResource); for (RuntimeSearchParam nextSpDef : searchParams) { @@ -187,7 +187,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements */ @Override public HashSet extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) { - HashSet retVal = new HashSet(); + HashSet retVal = new HashSet<>(); Collection searchParams = getSearchParams(theResource); for (RuntimeSearchParam nextSpDef : searchParams) { @@ -290,7 +290,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements */ @Override public Set extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) { - HashSet retVal = new HashSet(); + HashSet retVal = new HashSet<>(); Collection searchParams = getSearchParams(theResource); for (RuntimeSearchParam nextSpDef : searchParams) { @@ -354,7 +354,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements */ @Override public Set extractSearchParamStrings(ResourceTable theEntity, IBaseResource theResource) { - HashSet retVal = new HashSet(); + HashSet retVal = new HashSet<>(); String resourceName = getContext().getResourceDefinition(theResource).getName(); @@ -397,7 +397,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements addSearchTerm(theEntity, retVal, nextSpName, searchTerm); } else { if (nextObject instanceof HumanName) { - ArrayList allNames = new ArrayList(); + ArrayList allNames = new ArrayList<>(); HumanName nextHumanName = (HumanName) nextObject; if (isNotBlank(nextHumanName.getFamily())) { allNames.add(nextHumanName.getFamilyElement()); @@ -407,7 +407,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements addSearchTerm(theEntity, retVal, nextSpName, nextName.getValue()); } } else if (nextObject instanceof Address) { - ArrayList allNames = new ArrayList(); + ArrayList allNames = new ArrayList<>(); Address nextAddress = (Address) nextObject; allNames.addAll(nextAddress.getLine()); allNames.add(nextAddress.getCityElement()); @@ -573,7 +573,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements assert systems.size() == codes.size() : "Systems contains " + systems + ", codes contains: " + codes; - Set> haveValues = new HashSet>(); + Set> haveValues = new HashSet<>(); for (int i = 0; i < systems.size(); i++) { String system = systems.get(i); String code = codes.get(i); @@ -608,7 +608,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements @Override public Set extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) { - HashSet retVal = new HashSet(); + HashSet retVal = new HashSet<>(); Collection searchParams = getSearchParams(theResource); for (RuntimeSearchParam nextSpDef : searchParams) { @@ -690,7 +690,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements IWorkerContext worker = new org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext(getContext(), myValidationSupport); FHIRPathEngine fp = new FHIRPathEngine(worker); - List values = new ArrayList(); + List values = new ArrayList<>(); try { String[] nextPathsSplit = SPLIT.split(thePaths); for (String nextPath : nextPathsSplit) { @@ -717,7 +717,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements @Override public List extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) { - ArrayList retVal = new ArrayList(); + ArrayList retVal = new ArrayList<>(); String[] nextPathsSplit = SPLIT.split(theNextSpDef.getPath()); for (String path : nextPathsSplit) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java index 42cbf10057f..8d8d270471c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java @@ -30,7 +30,7 @@ import java.util.Collection; import java.util.Date; @MappedSuperclass -public abstract class BaseHasResource { +public abstract class BaseHasResource implements IBaseResourceEntity { @Column(name = "RES_DELETED_AT", nullable = true) @Temporal(TemporalType.TIMESTAMP) @@ -42,7 +42,7 @@ public abstract class BaseHasResource { @OptimisticLock(excluded = true) private FhirVersionEnum myFhirVersion; - @OneToOne(optional = true, fetch = FetchType.EAGER, cascade = {}, orphanRemoval = false) + @OneToOne(optional = true, fetch = FetchType.LAZY, cascade = {}, orphanRemoval = false) @JoinColumn(name = "FORCED_ID_PID") @OptimisticLock(excluded = true) private ForcedId myForcedId; @@ -63,6 +63,7 @@ public abstract class BaseHasResource { public abstract BaseTag addTag(TagDefinition theDef); + @Override public Date getDeleted() { return myDeleted; } @@ -72,6 +73,7 @@ public abstract class BaseHasResource { } + @Override public FhirVersionEnum getFhirVersion() { return myFhirVersion; } @@ -88,10 +90,13 @@ public abstract class BaseHasResource { myForcedId = theForcedId; } + @Override public abstract Long getId(); + @Override public abstract IdDt getIdDt(); + @Override public InstantDt getPublished() { if (myPublished != null) { return new InstantDt(myPublished); @@ -104,12 +109,15 @@ public abstract class BaseHasResource { myPublished = thePublished; } + @Override public abstract Long getResourceId(); + @Override public abstract String getResourceType(); public abstract Collection getTags(); + @Override public InstantDt getUpdated() { return new InstantDt(myUpdated); } @@ -118,12 +126,15 @@ public abstract class BaseHasResource { myUpdated = theUpdated; } + @Override public Date getUpdatedDate() { return myUpdated; } + @Override public abstract long getVersion(); + @Override public boolean isHasTags() { return myHasTags; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java index e27becf4200..97890d45032 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseResourceIndexedSearchParam.java @@ -36,13 +36,15 @@ import java.util.Date; @MappedSuperclass public abstract class BaseResourceIndexedSearchParam implements Serializable { - /** Don't change this without careful consideration. You will break existing hashes! */ - private static final HashFunction HASH_FUNCTION = Hashing.murmur3_128(0); - /** Don't make this public 'cause nobody better touch it! */ - private static final byte[] DELIMITER_BYTES = "|".getBytes(Charsets.UTF_8); - static final int MAX_SP_NAME = 100; - + /** + * Don't change this without careful consideration. You will break existing hashes! + */ + private static final HashFunction HASH_FUNCTION = Hashing.murmur3_128(0); + /** + * Don't make this public 'cause nobody better be able to modify it! + */ + private static final byte[] DELIMITER_BYTES = "|".getBytes(Charsets.UTF_8); private static final long serialVersionUID = 1L; // TODO: make this nullable=false and a primitive (written may 2017) @@ -71,6 +73,13 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { @Temporal(TemporalType.TIMESTAMP) private Date myUpdated; + /** + * Subclasses may override + */ + protected void clearHashes() { + // nothing + } + protected abstract Long getId(); public String getParamName() { @@ -82,13 +91,6 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { myParamName = theName; } - /** - * Subclasses may override - */ - protected void clearHashes() { - // nothing - } - public ResourceTable getResource() { return myResource; } @@ -127,6 +129,10 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { public abstract IQueryParameterType toQueryParameterType(); + public static long calculateHashIdentity(String theResourceType, String theParamName) { + return hash(theResourceType, theParamName); + } + /** * Applies a fast and consistent hashing algorithm to a set of strings */ @@ -148,5 +154,4 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable { return hashCode.asLong(); } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java index 80dbb1a7488..0c451533c70 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ForcedId.java @@ -20,30 +20,22 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.ForeignKey; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.JoinColumn; -import javax.persistence.OneToOne; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; - import org.hibernate.annotations.ColumnDefault; -//@formatter:off +import javax.persistence.*; + @Entity() @Table(name = "HFJ_FORCED_ID", uniqueConstraints = { @UniqueConstraint(name = "IDX_FORCEDID_RESID", columnNames = {"RESOURCE_PID"}), - @UniqueConstraint(name = "IDX_FORCEDID_TYPE_RESID", columnNames = {"RESOURCE_TYPE", "RESOURCE_PID"}) -}, indexes= { - @Index(name = "IDX_FORCEDID_TYPE_FORCEDID", columnList = "RESOURCE_TYPE,FORCED_ID"), + @UniqueConstraint(name = "IDX_FORCEDID_TYPE_FID", columnNames = {"RESOURCE_TYPE", "FORCED_ID"}) +}, indexes = { + /* + * NB: We previously had indexes named + * - IDX_FORCEDID_TYPE_FORCEDID + * - IDX_FORCEDID_TYPE_RESID + * so don't reuse these names + */ }) -//@formatter:on public class ForcedId { public static final int MAX_FORCED_ID_LENGTH = 100; @@ -57,11 +49,11 @@ public class ForcedId { @Column(name = "PID") private Long myId; - @JoinColumn(name = "RESOURCE_PID", nullable = false, updatable = false, foreignKey=@ForeignKey(name="FK_FORCEDID_RESOURCE")) + @JoinColumn(name = "RESOURCE_PID", nullable = false, updatable = false, foreignKey = @ForeignKey(name = "FK_FORCEDID_RESOURCE")) @OneToOne() private ResourceTable myResource; - @Column(name = "RESOURCE_PID", nullable = false, updatable = false, insertable=false) + @Column(name = "RESOURCE_PID", nullable = false, updatable = false, insertable = false) private Long myResourcePid; // This is updatable=true because it was added in 1.6 and needs to be set.. At some @@ -81,39 +73,39 @@ public class ForcedId { return myForcedId; } - public ResourceTable getResource() { - return myResource; - } - - public Long getResourcePid() { - if (myResourcePid==null) { - return myResource.getId(); - } - return myResourcePid; - } - - public String getResourceType() { - return myResourceType; - } - public void setForcedId(String theForcedId) { myForcedId = theForcedId; } + public ResourceTable getResource() { + return myResource; + } + public void setResource(ResourceTable theResource) { myResource = theResource; } - public void setResourcePid(Long theResourcePid) { - myResourcePid = theResourcePid; + public Long getResourcePid() { + if (myResourcePid == null) { + return myResource.getId(); + } + return myResourcePid; } public void setResourcePid(ResourceTable theResourcePid) { myResource = theResourcePid; } + public String getResourceType() { + return myResourceType; + } + public void setResourceType(String theResourceType) { myResourceType = theResourceType; } + public void setResourcePid(Long theResourcePid) { + myResourcePid = theResourcePid; + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/IBaseResourceEntity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/IBaseResourceEntity.java new file mode 100644 index 00000000000..157818ee740 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/IBaseResourceEntity.java @@ -0,0 +1,41 @@ +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 java.util.Date; + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.InstantDt; + +public interface IBaseResourceEntity { + + Date getDeleted(); + FhirVersionEnum getFhirVersion(); + Long getId(); + IdDt getIdDt(); + InstantDt getPublished(); + Long getResourceId(); + String getResourceType(); + InstantDt getUpdated(); + Date getUpdatedDate(); + long getVersion(); + boolean isHasTags(); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java index 4661405a2bc..7ac859e262f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamCoords.java @@ -31,30 +31,32 @@ import javax.persistence.*; @Embeddable @Entity -@Table(name = "HFJ_SPIDX_COORDS", indexes = { - @Index(name = "IDX_SP_COORDS", columnList = "RES_TYPE,SP_NAME,SP_LATITUDE,SP_LONGITUDE"), - @Index(name = "IDX_SP_COORDS_UPDATED", columnList = "SP_UPDATED"), - @Index(name = "IDX_SP_COORDS_RESID", columnList = "RES_ID") +@Table(name = "HFJ_SPIDX_COORDS", indexes = { + @Index(name = "IDX_SP_COORDS", columnList = "RES_TYPE,SP_NAME,SP_LATITUDE,SP_LONGITUDE"), + @Index(name = "IDX_SP_COORDS_UPDATED", columnList = "SP_UPDATED"), + @Index(name = "IDX_SP_COORDS_RESID", columnList = "RES_ID") }) public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchParam { public static final int MAX_LENGTH = 100; private static final long serialVersionUID = 1L; - + @Column(name = "SP_LATITUDE") + @Field + public double myLatitude; + @Column(name = "SP_LONGITUDE") + @Field + public double myLongitude; @Id @SequenceGenerator(name = "SEQ_SPIDX_COORDS", sequenceName = "SEQ_SPIDX_COORDS") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_COORDS") @Column(name = "SP_ID") private Long myId; - - @Column(name = "SP_LATITUDE") - @Field - public double myLatitude; - - @Column(name = "SP_LONGITUDE") - @Field - public double myLongitude; + /** + * @since 3.5.0 - At some point this should be made not-null + */ + @Column(name = "HASH_IDENTITY", nullable = true) + private Long myHashIdentity; public ResourceIndexedSearchParamCoords() { } @@ -65,6 +67,20 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP setLongitude(theLongitude); } + @PrePersist + public void calculateHashes() { + if (myHashIdentity == null) { + String resourceType = getResourceType(); + String paramName = getParamName(); + setHashIdentity(calculateHashIdentity(resourceType, paramName)); + } + } + + @Override + protected void clearHashes() { + myHashIdentity = null; + } + @Override public boolean equals(Object theObj) { if (this == theObj) { @@ -82,27 +98,39 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP b.append(getResource(), obj.getResource()); b.append(getLatitude(), obj.getLatitude()); b.append(getLongitude(), obj.getLongitude()); + b.append(getHashIdentity(), obj.getHashIdentity()); return b.isEquals(); } + public Long getHashIdentity() { + return myHashIdentity; + } + + public void setHashIdentity(Long theHashIdentity) { + myHashIdentity = theHashIdentity; + } + @Override protected Long getId() { return myId; } - @Override - public IQueryParameterType toQueryParameterType() { - return null; - } - public double getLatitude() { return myLatitude; } + public void setLatitude(double theLatitude) { + myLatitude = theLatitude; + } + public double getLongitude() { return myLongitude; } + public void setLongitude(double theLongitude) { + myLongitude = theLongitude; + } + @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); @@ -113,12 +141,9 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP return b.toHashCode(); } - public void setLatitude(double theLatitude) { - myLatitude = theLatitude; - } - - public void setLongitude(double theLongitude) { - myLongitude = theLongitude; + @Override + public IQueryParameterType toQueryParameterType() { + return null; } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java index 858a20de253..63e54be4b30 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamDate.java @@ -37,17 +37,14 @@ import java.util.Date; @Embeddable @Entity @Table(name = "HFJ_SPIDX_DATE", indexes = { - @Index(name = "IDX_SP_DATE", columnList = "RES_TYPE,SP_NAME,SP_VALUE_LOW,SP_VALUE_HIGH"), +// @Index(name = "IDX_SP_DATE", columnList = "RES_TYPE,SP_NAME,SP_VALUE_LOW,SP_VALUE_HIGH"), + @Index(name = "IDX_SP_DATE_HASH", columnList = "HASH_IDENTITY,SP_VALUE_LOW,SP_VALUE_HIGH"), @Index(name = "IDX_SP_DATE_UPDATED", columnList = "SP_UPDATED"), @Index(name = "IDX_SP_DATE_RESID", columnList = "RES_ID") }) public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchParam { private static final long serialVersionUID = 1L; - - @Transient - private transient String myOriginalValue; - @Column(name = "SP_VALUE_HIGH", nullable = true) @Temporal(TemporalType.TIMESTAMP) @Field @@ -56,11 +53,18 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar @Temporal(TemporalType.TIMESTAMP) @Field public Date myValueLow; + @Transient + private transient String myOriginalValue; @Id @SequenceGenerator(name = "SEQ_SPIDX_DATE", sequenceName = "SEQ_SPIDX_DATE") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_DATE") @Column(name = "SP_ID") private Long myId; + /** + * @since 3.5.0 - At some point this should be made not-null + */ + @Column(name = "HASH_IDENTITY", nullable = true) + private Long myHashIdentity; /** * Constructor @@ -79,6 +83,20 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar myOriginalValue = theOriginalValue; } + @PrePersist + public void calculateHashes() { + if (myHashIdentity == null) { + String resourceType = getResourceType(); + String paramName = getParamName(); + setHashIdentity(calculateHashIdentity(resourceType, paramName)); + } + } + + @Override + protected void clearHashes() { + myHashIdentity = null; + } + @Override public boolean equals(Object theObj) { if (this == theObj) { @@ -97,9 +115,23 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar b.append(getResource(), obj.getResource()); b.append(getTimeFromDate(getValueHigh()), getTimeFromDate(obj.getValueHigh())); b.append(getTimeFromDate(getValueLow()), getTimeFromDate(obj.getValueLow())); + b.append(getHashIdentity(), obj.getHashIdentity()); return b.isEquals(); } + public Long getHashIdentity() { + return myHashIdentity; + } + + public void setHashIdentity(Long theHashIdentity) { + myHashIdentity = theHashIdentity; + } + + @Override + protected Long getId() { + return myId; + } + protected Long getTimeFromDate(Date date) { if (date != null) { return date.getTime(); @@ -107,11 +139,6 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar return null; } - @Override - protected Long getId() { - return myId; - } - public Date getValueHigh() { return myValueHigh; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java index 0232ff25ad9..71d5f0b56fb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamNumber.java @@ -34,15 +34,14 @@ import org.hibernate.search.annotations.NumericField; import javax.persistence.*; import java.math.BigDecimal; -//@formatter:off @Embeddable @Entity @Table(name = "HFJ_SPIDX_NUMBER", indexes = { - @Index(name = "IDX_SP_NUMBER", columnList = "RES_TYPE,SP_NAME,SP_VALUE"), +// @Index(name = "IDX_SP_NUMBER", columnList = "RES_TYPE,SP_NAME,SP_VALUE"), + @Index(name = "IDX_SP_NUMBER_HASH_VAL", columnList = "HASH_IDENTITY,SP_VALUE"), @Index(name = "IDX_SP_NUMBER_UPDATED", columnList = "SP_UPDATED"), @Index(name = "IDX_SP_NUMBER_RESID", columnList = "RES_ID") }) -//@formatter:on public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchParam { private static final long serialVersionUID = 1L; @@ -56,6 +55,11 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_NUMBER") @Column(name = "SP_ID") private Long myId; + /** + * @since 3.5.0 - At some point this should be made not-null + */ + @Column(name = "HASH_IDENTITY", nullable = true) + private Long myHashIdentity; public ResourceIndexedSearchParamNumber() { } @@ -65,6 +69,20 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP setValue(theValue); } + @PrePersist + public void calculateHashes() { + if (myHashIdentity == null) { + String resourceType = getResourceType(); + String paramName = getParamName(); + setHashIdentity(calculateHashIdentity(resourceType, paramName)); + } + } + + @Override + protected void clearHashes() { + myHashIdentity = null; + } + @Override public boolean equals(Object theObj) { if (this == theObj) { @@ -82,9 +100,18 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP b.append(getResource(), obj.getResource()); b.append(getValue(), obj.getValue()); b.append(isMissing(), obj.isMissing()); + b.append(getHashIdentity(), obj.getHashIdentity()); return b.isEquals(); } + public Long getHashIdentity() { + return myHashIdentity; + } + + public void setHashIdentity(Long theHashIdentity) { + myHashIdentity = theHashIdentity; + } + @Override protected Long getId() { return myId; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java index 1398d6913a2..8eb9caddc96 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantity.java @@ -33,13 +33,14 @@ import org.hibernate.search.annotations.NumericField; import javax.persistence.*; import java.math.BigDecimal; -import java.math.RoundingMode; //@formatter:off @Embeddable @Entity @Table(name = "HFJ_SPIDX_QUANTITY", indexes = { - @Index(name = "IDX_SP_QUANTITY", columnList = "RES_TYPE,SP_NAME,SP_SYSTEM,SP_UNITS,SP_VALUE"), +// @Index(name = "IDX_SP_QUANTITY", columnList = "RES_TYPE,SP_NAME,SP_SYSTEM,SP_UNITS,SP_VALUE"), + @Index(name = "IDX_SP_QUANTITY_HASH", columnList = "HASH_IDENTITY,SP_VALUE"), + @Index(name = "IDX_SP_QUANTITY_HASH_UN", columnList = "HASH_IDENTITY_AND_UNITS,SP_VALUE"), @Index(name = "IDX_SP_QUANTITY_UPDATED", columnList = "SP_UPDATED"), @Index(name = "IDX_SP_QUANTITY_RESID", columnList = "RES_ID") }) @@ -66,20 +67,26 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc @Column(name = "SP_ID") private Long myId; /** - * @since 3.4.0 - At some point this should be made not-null + * @since 3.5.0 - At some point this should be made not-null */ - @Column(name = "HASH_UNITS_AND_VALPREFIX", nullable = true) - private Long myHashUnitsAndValPrefix; + @Column(name = "HASH_IDENTITY_AND_UNITS", nullable = true) + private Long myHashIdentityAndUnits; /** - * @since 3.4.0 - At some point this should be made not-null + * @since 3.5.0 - At some point this should be made not-null */ - @Column(name = "HASH_VALPREFIX", nullable = true) - private Long myHashValPrefix; + @Column(name = "HASH_IDENTITY_SYS_UNITS", nullable = true) + private Long myHashIdentitySystemAndUnits; + /** + * @since 3.5.0 - At some point this should be made not-null + */ + @Column(name = "HASH_IDENTITY", nullable = true) + private Long myHashIdentity; public ResourceIndexedSearchParamQuantity() { // nothing } + public ResourceIndexedSearchParamQuantity(String theParamName, BigDecimal theValue, String theSystem, String theUnits) { setParamName(theParamName); setSystem(theSystem); @@ -89,16 +96,21 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc @PrePersist public void calculateHashes() { - if (myHashUnitsAndValPrefix == null) { - setHashUnitsAndValPrefix(hash(getResourceType(), getParamName(), getSystem(), getUnits(), toTruncatedString(getValue()))); - setHashValPrefix(hash(getResourceType(), getParamName(), toTruncatedString(getValue()))); + if (myHashIdentity == null) { + String resourceType = getResourceType(); + String paramName = getParamName(); + String units = getUnits(); + String system = getSystem(); + setHashIdentity(calculateHashIdentity(resourceType, paramName)); + setHashIdentityAndUnits(calculateHashUnits(resourceType, paramName, units)); + setHashIdentitySystemAndUnits(calculateHashSystemAndUnits(resourceType, paramName, system, units)); } } @Override protected void clearHashes() { - myHashUnitsAndValPrefix = null; - myHashValPrefix = null; + myHashIdentity = null; + myHashIdentityAndUnits = null; } @Override @@ -119,27 +131,36 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc b.append(getSystem(), obj.getSystem()); b.append(getUnits(), obj.getUnits()); b.append(getValue(), obj.getValue()); - b.append(getHashUnitsAndValPrefix(), obj.getHashUnitsAndValPrefix()); - b.append(getHashValPrefix(), obj.getHashValPrefix()); + b.append(getHashIdentity(), obj.getHashIdentity()); + b.append(getHashIdentitySystemAndUnits(), obj.getHashIdentitySystemAndUnits()); + b.append(getHashIdentityAndUnits(), obj.getHashIdentityAndUnits()); return b.isEquals(); } - public Long getHashUnitsAndValPrefix() { + public Long getHashIdentity() { calculateHashes(); - return myHashUnitsAndValPrefix; + return myHashIdentity; } - public void setHashUnitsAndValPrefix(Long theHashUnitsAndValPrefix) { - myHashUnitsAndValPrefix = theHashUnitsAndValPrefix; + public void setHashIdentity(Long theHashIdentity) { + myHashIdentity = theHashIdentity; } - public Long getHashValPrefix() { + public Long getHashIdentityAndUnits() { calculateHashes(); - return myHashValPrefix; + return myHashIdentityAndUnits; } - public void setHashValPrefix(Long theHashValPrefix) { - myHashValPrefix = theHashValPrefix; + public void setHashIdentityAndUnits(Long theHashIdentityAndUnits) { + myHashIdentityAndUnits = theHashIdentityAndUnits; + } + + private Long getHashIdentitySystemAndUnits() { + return myHashIdentitySystemAndUnits; + } + + public void setHashIdentitySystemAndUnits(Long theHashIdentitySystemAndUnits) { + myHashIdentitySystemAndUnits = theHashIdentitySystemAndUnits; } @Override @@ -176,14 +197,13 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc @Override public int hashCode() { + calculateHashes(); HashCodeBuilder b = new HashCodeBuilder(); + b.append(getResourceType()); b.append(getParamName()); - b.append(getResource()); b.append(getSystem()); b.append(getUnits()); b.append(getValue()); - b.append(getHashUnitsAndValPrefix()); - b.append(getHashValPrefix()); return b.toHashCode(); } @@ -201,14 +221,16 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc b.append("units", getUnits()); b.append("value", getValue()); b.append("missing", isMissing()); + b.append("hashIdentitySystemAndUnits", myHashIdentitySystemAndUnits); return b.build(); } - private static String toTruncatedString(BigDecimal theValue) { - if (theValue == null) { - return null; - } - return theValue.setScale(0, RoundingMode.FLOOR).toPlainString(); + public static long calculateHashSystemAndUnits(String theResourceType, String theParamName, String theSystem, String theUnits) { + return hash(theResourceType, theParamName, theSystem, theUnits); + } + + public static long calculateHashUnits(String theResourceType, String theParamName, String theUnits) { + 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-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java index 133c8d78930..1ee6dfccd46 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamString.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ +import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.param.StringParam; import org.apache.commons.lang3.StringUtils; @@ -38,7 +39,14 @@ import static org.apache.commons.lang3.StringUtils.left; @Embeddable @Entity @Table(name = "HFJ_SPIDX_STRING", indexes = { - @Index(name = "IDX_SP_STRING", columnList = "RES_TYPE,SP_NAME,SP_VALUE_NORMALIZED"), + /* + * Note: We previously had indexes with the following names, + * do not reuse these names: + * IDX_SP_STRING + */ + @Index(name = "IDX_SP_STRING_HASH_NRM", columnList = "HASH_NORM_PREFIX,SP_VALUE_NORMALIZED"), + @Index(name = "IDX_SP_STRING_HASH_EXCT", columnList = "HASH_EXACT"), + @Index(name = "IDX_SP_STRING_UPDATED", columnList = "SP_UPDATED"), @Index(name = "IDX_SP_STRING_RESID", columnList = "RES_ID") }) @@ -127,13 +135,16 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP */ @Column(name = "HASH_EXACT", nullable = true) private Long myHashExact; + @Transient + private transient DaoConfig myDaoConfig; public ResourceIndexedSearchParamString() { super(); } - public ResourceIndexedSearchParamString(String theName, String theValueNormalized, String theValueExact) { + public ResourceIndexedSearchParamString(DaoConfig theDaoConfig, String theName, String theValueNormalized, String theValueExact) { + setDaoConfig(theDaoConfig); setParamName(theName); setValueNormalized(theValueNormalized); setValueExact(theValueExact); @@ -141,9 +152,13 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP @PrePersist public void calculateHashes() { - if (myHashNormalizedPrefix == null) { - setHashNormalizedPrefix(hash(getResourceType(), getParamName(), left(getValueNormalized(), HASH_PREFIX_LENGTH))); - setHashExact(hash(getResourceType(), getParamName(), getValueExact())); + if (myHashNormalizedPrefix == null && myDaoConfig != null) { + String resourceType = getResourceType(); + String paramName = getParamName(); + String valueNormalized = getValueNormalized(); + String valueExact = getValueExact(); + setHashNormalizedPrefix(calculateHashNormalized(myDaoConfig, resourceType, paramName, valueNormalized)); + setHashExact(calculateHashExact(resourceType, paramName, valueExact)); } } @@ -169,8 +184,8 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP b.append(getParamName(), obj.getParamName()); b.append(getResource(), obj.getResource()); b.append(getValueExact(), obj.getValueExact()); - b.append(getHashNormalizedPrefix(), obj.getHashNormalizedPrefix()); b.append(getHashExact(), obj.getHashExact()); + b.append(getHashNormalizedPrefix(), obj.getHashNormalizedPrefix()); return b.isEquals(); } @@ -225,11 +240,14 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP b.append(getParamName()); b.append(getResource()); b.append(getValueExact()); - b.append(getHashNormalizedPrefix()); - b.append(getHashExact()); return b.toHashCode(); } + public BaseResourceIndexedSearchParam setDaoConfig(DaoConfig theDaoConfig) { + myDaoConfig = theDaoConfig; + return this; + } + @Override public IQueryParameterType toQueryParameterType() { return new StringParam(getValueExact()); @@ -244,4 +262,23 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP return b.build(); } + public static long calculateHashExact(String theResourceType, String theParamName, String theValueExact) { + return hash(theResourceType, theParamName, theValueExact); + } + + public static long calculateHashNormalized(DaoConfig theDaoConfig, 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 + * make the hash even more unique, which will be good for + * performance. + */ + int hashPrefixLength = HASH_PREFIX_LENGTH; + if (theDaoConfig.isAllowContainsSearches()) { + hashPrefixLength = 0; + } + + return hash(theResourceType, theParamName, left(theValueNormalized, hashPrefixLength)); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java index 55a4c8c00c7..75938fb79e8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java @@ -31,11 +31,23 @@ import org.hibernate.search.annotations.Field; import javax.persistence.*; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.trim; + @Embeddable @Entity @Table(name = "HFJ_SPIDX_TOKEN", indexes = { - @Index(name = "IDX_SP_TOKEN", columnList = "RES_TYPE,SP_NAME,SP_SYSTEM,SP_VALUE"), - @Index(name = "IDX_SP_TOKEN_UNQUAL", columnList = "RES_TYPE,SP_NAME,SP_VALUE"), + /* + * Note: We previously had indexes with the following names, + * do not reuse these names: + * IDX_SP_TOKEN + * IDX_SP_TOKEN_UNQUAL + */ + @Index(name = "IDX_SP_TOKEN_HASH", columnList = "HASH_IDENTITY"), + @Index(name = "IDX_SP_TOKEN_HASH_S", columnList = "HASH_SYS"), + @Index(name = "IDX_SP_TOKEN_HASH_SV", columnList = "HASH_SYS_AND_VALUE"), + @Index(name = "IDX_SP_TOKEN_HASH_V", columnList = "HASH_VALUE"), + @Index(name = "IDX_SP_TOKEN_UPDATED", columnList = "SP_UPDATED"), @Index(name = "IDX_SP_TOKEN_RESID", columnList = "RES_ID") }) @@ -50,12 +62,18 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa public String mySystem; @Field() @Column(name = "SP_VALUE", nullable = true, length = MAX_LENGTH) - public String myValue; + private String myValue; + @SuppressWarnings("unused") @Id @SequenceGenerator(name = "SEQ_SPIDX_TOKEN", sequenceName = "SEQ_SPIDX_TOKEN") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_TOKEN") @Column(name = "SP_ID") private Long myId; + /** + * @since 3.4.0 - At some point this should be made not-null + */ + @Column(name = "HASH_IDENTITY", nullable = true) + private Long myHashIdentity; /** * @since 3.4.0 - At some point this should be made not-null */ @@ -90,17 +108,20 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa setValue(theValue); } - @PrePersist public void calculateHashes() { if (myHashSystem == null) { - setHashSystem(hash(getResourceType(), getParamName(), getSystem())); - setHashSystemAndValue(hash(getResourceType(), getParamName(), getSystem(), getValue())); - setHashValue(hash(getResourceType(), getParamName(), getValue())); + String resourceType = getResourceType(); + String paramName = getParamName(); + String system = getSystem(); + String value = getValue(); + setHashIdentity(calculateHashIdentity(resourceType, paramName)); + setHashSystem(calculateHashSystem(resourceType, paramName, system)); + setHashSystemAndValue(calculateHashSystemAndValue(resourceType, paramName, system, value)); + setHashValue(calculateHashValue(resourceType, paramName, value)); } } - @Override protected void clearHashes() { myHashSystem = null; @@ -125,37 +146,47 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa b.append(getResource(), obj.getResource()); b.append(getSystem(), obj.getSystem()); b.append(getValue(), obj.getValue()); + b.append(getHashIdentity(), obj.getHashIdentity()); b.append(getHashSystem(), obj.getHashSystem()); b.append(getHashSystemAndValue(), obj.getHashSystemAndValue()); b.append(getHashValue(), obj.getHashValue()); return b.isEquals(); } - public Long getHashSystem() { + Long getHashSystem() { calculateHashes(); return myHashSystem; } - public void setHashSystem(Long theHashSystem) { + private Long getHashIdentity() { + calculateHashes(); + return myHashIdentity; + } + + private void setHashIdentity(Long theHashIdentity) { + myHashIdentity = theHashIdentity; + } + + private void setHashSystem(Long theHashSystem) { myHashSystem = theHashSystem; } - public Long getHashSystemAndValue() { + Long getHashSystemAndValue() { calculateHashes(); return myHashSystemAndValue; } - public void setHashSystemAndValue(Long theHashSystemAndValue) { + private void setHashSystemAndValue(Long theHashSystemAndValue) { calculateHashes(); myHashSystemAndValue = theHashSystemAndValue; } - public Long getHashValue() { + Long getHashValue() { calculateHashes(); return myHashValue; } - public void setHashValue(Long theHashValue) { + private void setHashValue(Long theHashValue) { myHashValue = theHashValue; } @@ -184,18 +215,15 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa @Override public int hashCode() { + calculateHashes(); HashCodeBuilder b = new HashCodeBuilder(); b.append(getParamName()); b.append(getResource()); b.append(getSystem()); b.append(getValue()); - b.append(getHashSystem()); - b.append(getHashSystemAndValue()); - b.append(getHashValue()); return b.toHashCode(); } - @Override public IQueryParameterType toQueryParameterType() { return new TokenParam(getSystem(), getValue()); @@ -210,4 +238,16 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa b.append("value", getValue()); return b.build(); } + + public static long calculateHashSystem(String theResourceType, String theParamName, String theSystem) { + return hash(theResourceType, theParamName, trim(theSystem)); + } + + public static long calculateHashSystemAndValue(String theResourceType, String theParamName, String theSystem, String theValue) { + return hash(theResourceType, theParamName, defaultString(trim(theSystem)), trim(theValue)); + } + + 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-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java index 27b52efeac9..6f35d8809e9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamUri.java @@ -34,6 +34,8 @@ import javax.persistence.*; @Entity @Table(name = "HFJ_SPIDX_URI", indexes = { @Index(name = "IDX_SP_URI", columnList = "RES_TYPE,SP_NAME,SP_URI"), + @Index(name = "IDX_SP_URI_HASH_IDENTITY", columnList = "HASH_IDENTITY,SP_URI"), + @Index(name = "IDX_SP_URI_HASH_URI", columnList = "HASH_URI"), @Index(name = "IDX_SP_URI_RESTYPE_NAME", columnList = "RES_TYPE,SP_NAME"), @Index(name = "IDX_SP_URI_UPDATED", columnList = "SP_UPDATED"), @Index(name = "IDX_SP_URI_COORDS", columnList = "RES_ID") @@ -59,11 +61,17 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara */ @Column(name = "HASH_URI", nullable = true) private Long myHashUri; + /** + * @since 3.5.0 - At some point this should be made not-null + */ + @Column(name = "HASH_IDENTITY", nullable = true) + private Long myHashIdentity; /** * Constructor */ public ResourceIndexedSearchParamUri() { + super(); } /** @@ -77,7 +85,11 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara @PrePersist public void calculateHashes() { if (myHashUri == null) { - setHashUri(hash(getResourceType(), getParamName(), getUri())); + String resourceType = getResourceType(); + String paramName = getParamName(); + String uri = getUri(); + setHashIdentity(calculateHashIdentity(resourceType, paramName)); + setHashUri(calculateHashUri(resourceType, paramName, uri)); } } @@ -103,9 +115,18 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara b.append(getResource(), obj.getResource()); b.append(getUri(), obj.getUri()); b.append(getHashUri(), obj.getHashUri()); + b.append(getHashIdentity(), obj.getHashIdentity()); return b.isEquals(); } + private Long getHashIdentity() { + return myHashIdentity; + } + + private void setHashIdentity(long theHashIdentity) { + myHashIdentity = theHashIdentity; + } + public Long getHashUri() { calculateHashes(); return myHashUri; @@ -153,4 +174,8 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara return b.toString(); } + 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-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java index 00310269e56..8f027ce5276 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceLink.java @@ -66,14 +66,12 @@ public class ResourceLink implements Serializable { @ManyToOne(optional = false, fetch=FetchType.LAZY) @JoinColumn(name = "SRC_RESOURCE_ID", referencedColumnName = "RES_ID", nullable = false, foreignKey=@ForeignKey(name="FK_RESLINK_SOURCE")) -// @ContainedIn() private ResourceTable mySourceResource; @Column(name = "SRC_RESOURCE_ID", insertable = false, updatable = false, nullable = false) private Long mySourceResourcePid; @Column(name = "SOURCE_RESOURCE_TYPE", nullable=false, length=ResourceTable.RESTYPE_LEN) - @ColumnDefault("''") // TODO: remove this (it's only here for simplifying upgrades of 1.3 -> 1.4) @Field() private String mySourceResourceType; @@ -86,7 +84,6 @@ public class ResourceLink implements Serializable { private Long myTargetResourcePid; @Column(name = "TARGET_RESOURCE_TYPE", nullable=false, length=ResourceTable.RESTYPE_LEN) - @ColumnDefault("''") // TODO: remove this (it's only here for simplifying upgrades of 1.3 -> 1.4) @Field() private String myTargetResourceType; 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 new file mode 100644 index 00000000000..e62712ebbe7 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceSearchView.java @@ -0,0 +1,200 @@ +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 java.io.Serializable; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Subselect; + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.rest.api.Constants; + +//@formatter:off +@Entity +@Immutable +@Subselect("SELECT h.pid as pid " + + ", h.res_id as res_id " + + ", h.res_type as res_type " + + ", h.res_version as res_version " + // FHIR version + ", h.res_ver as res_ver " + // resource version + ", h.has_tags as has_tags " + + ", h.res_deleted_at as res_deleted_at " + + ", h.res_published as res_published " + + ", h.res_updated as res_updated " + + ", h.res_text as res_text " + + ", h.res_encoding as res_encoding " + + ", f.forced_id as forced_pid " + + "FROM HFJ_RES_VER h " + + " LEFT OUTER JOIN HFJ_FORCED_ID f ON f.resource_pid = h.res_id " + + " INNER JOIN HFJ_RESOURCE r ON r.res_id = h.res_id and r.res_ver = h.res_ver") +// @formatter:on +public class ResourceSearchView implements IBaseResourceEntity, Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "PID") + private Long myId; + + @Column(name = "RES_ID") + private Long myResourceId; + + @Column(name = "RES_TYPE") + private String myResourceType; + + @Column(name = "RES_VERSION") + @Enumerated(EnumType.STRING) + private FhirVersionEnum myFhirVersion; + + @Column(name = "RES_VER") + private Long myResourceVersion; + + @Column(name = "HAS_TAGS") + private boolean myHasTags; + + @Column(name = "RES_DELETED_AT") + @Temporal(TemporalType.TIMESTAMP) + private Date myDeleted; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "RES_PUBLISHED") + private Date myPublished; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "RES_UPDATED") + private Date myUpdated; + + @Column(name = "RES_TEXT") + @Lob() + private byte[] myResource; + + @Column(name = "RES_ENCODING") + @Enumerated(EnumType.STRING) + private ResourceEncodingEnum myEncoding; + + @Column(name = "forced_pid") + private String myForcedPid; + + public ResourceSearchView() { + } + + @Override + public Date getDeleted() { + return myDeleted; + } + + public void setDeleted(Date theDate) { + myDeleted = theDate; + } + + @Override + public FhirVersionEnum getFhirVersion() { + return myFhirVersion; + } + + public void setFhirVersion(FhirVersionEnum theFhirVersion) { + myFhirVersion = theFhirVersion; + } + + public String getForcedId() { + return myForcedPid; + } + + @Override + public Long getId() { + return myResourceId; + } + + @Override + public IdDt getIdDt() { + if (myForcedPid == null) { + Long id = myResourceId; + return new IdDt(myResourceType + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + getVersion()); + } else { + return new IdDt( + getResourceType() + '/' + getForcedId() + '/' + Constants.PARAM_HISTORY + '/' + getVersion()); + } + } + + @Override + public InstantDt getPublished() { + if (myPublished != null) { + return new InstantDt(myPublished); + } else { + return null; + } + } + + public void setPublished(Date thePublished) { + myPublished = thePublished; + } + + @Override + public Long getResourceId() { + return myResourceId; + } + + @Override + public String getResourceType() { + return myResourceType; + } + + @Override + public InstantDt getUpdated() { + return new InstantDt(myUpdated); + } + + @Override + public Date getUpdatedDate() { + return myUpdated; + } + + @Override + public long getVersion() { + return myResourceVersion; + } + + @Override + public boolean isHasTags() { + return myHasTags; + } + + public byte[] getResource() { + return myResource; + } + + public ResourceEncodingEnum getEncoding() { + return myEncoding; + } + +} 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 7cc713ed0ef..d357611b123 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 @@ -94,10 +94,9 @@ public class Search implements Serializable { @OneToMany(mappedBy="mySearch") private Collection myResults; - // TODO: change nullable to false after 2.5 @NotNull @Temporal(TemporalType.TIMESTAMP) - @Column(name="SEARCH_LAST_RETURNED", nullable=true, updatable=false) + @Column(name="SEARCH_LAST_RETURNED", nullable=false, updatable=false) private Date mySearchLastReturned; @Lob() diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParam.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParam.java deleted file mode 100644 index 79cb7398f63..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParam.java +++ /dev/null @@ -1,59 +0,0 @@ -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 javax.persistence.*; - -@Entity -@Table(name = "HFJ_SEARCH_PARM", uniqueConstraints= { - @UniqueConstraint(name="IDX_SEARCHPARM_RESTYPE_SPNAME", columnNames= {"RES_TYPE", "PARAM_NAME"}) -}) -public class SearchParam { - - @Id - @SequenceGenerator(name = "SEQ_SEARCHPARM_ID", sequenceName = "SEQ_SEARCHPARM_ID") - @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SEARCHPARM_ID") - @Column(name = "PID") - private Long myId; - - @Column(name="PARAM_NAME", length=BaseResourceIndexedSearchParam.MAX_SP_NAME, nullable=false, updatable=false) - private String myParamName; - - @Column(name="RES_TYPE", length=ResourceTable.RESTYPE_LEN, nullable=false, updatable=false) - private String myResourceName; - - public String getParamName() { - return myParamName; - } - - public void setParamName(String theParamName) { - myParamName = theParamName; - } - - public void setResourceName(String theResourceName) { - myResourceName = theResourceName; - } - - public Long getId() { - return myId; - } - -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParamPresent.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParamPresent.java index 06082ba06bd..d5245966225 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParamPresent.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/SearchParamPresent.java @@ -20,18 +20,16 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import java.io.Serializable; - -import javax.persistence.*; - import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import javax.persistence.*; +import java.io.Serializable; + @Entity @Table(name = "HFJ_RES_PARAM_PRESENT", indexes = { - @Index(name = "IDX_RESPARMPRESENT_RESID", columnList = "RES_ID") -}, uniqueConstraints = { - @UniqueConstraint(name = "IDX_RESPARMPRESENT_SPID_RESID", columnNames = { "SP_ID", "RES_ID" }) + @Index(name = "IDX_RESPARMPRESENT_RESID", columnList = "RES_ID"), + @Index(name = "IDX_RESPARMPRESENT_HASHPRES", columnList = "HASH_PRESENCE") }) public class SearchParamPresent implements Serializable { @@ -42,17 +40,15 @@ public class SearchParamPresent implements Serializable { @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESPARMPRESENT_ID") @Column(name = "PID") private Long myId; - @Column(name = "SP_PRESENT", nullable = false) private boolean myPresent; - @ManyToOne() @JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_RESPARMPRES_RESID")) private ResourceTable myResource; - - @ManyToOne() - @JoinColumn(name = "SP_ID", referencedColumnName = "PID", nullable = false, foreignKey = @ForeignKey(name = "FK_RESPARMPRES_SPID")) - private SearchParam mySearchParam; + @Transient + private transient String myParamName; + @Column(name = "HASH_PRESENCE") + private Long myHashPresence; /** * Constructor @@ -60,13 +56,40 @@ public class SearchParamPresent implements Serializable { public SearchParamPresent() { super(); } - + + @SuppressWarnings("unused") + @PrePersist + public void calculateHashes() { + if (myHashPresence == null) { + String resourceType = getResource().getResourceType(); + String paramName = getParamName(); + boolean present = myPresent; + setHashPresence(calculateHashPresence(resourceType, paramName, present)); + } + } + + public Long getHashPresence() { + return myHashPresence; + } + + public void setHashPresence(Long theHashPresence) { + myHashPresence = theHashPresence; + } + + public String getParamName() { + return myParamName; + } + + public void setParamName(String theParamName) { + myParamName = theParamName; + } + public ResourceTable getResource() { return myResource; } - public SearchParam getSearchParam() { - return mySearchParam; + public void setResource(ResourceTable theResourceTable) { + myResource = theResourceTable; } public boolean isPresent() { @@ -77,22 +100,18 @@ public class SearchParamPresent implements Serializable { myPresent = thePresent; } - public void setResource(ResourceTable theResourceTable) { - myResource = theResourceTable; - } - - public void setSearchParam(SearchParam theSearchParam) { - mySearchParam = theSearchParam; - } - @Override public String toString() { ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); - - b.append("res_pid", myResource.getIdDt().toUnqualifiedVersionless().getValue()); - b.append("param", mySearchParam.getParamName()); + + b.append("resPid", myResource.getIdDt().toUnqualifiedVersionless().getValue()); + b.append("paramName", myParamName); b.append("present", myPresent); return b.build(); } + public static long calculateHashPresence(String theResourceType, String theParamName, boolean thePresent) { + return BaseResourceIndexedSearchParam.hash(theResourceType, theParamName, Boolean.toString(thePresent)); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConcept.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConcept.java index b6d5deba6c0..b672aca316d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConcept.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConcept.java @@ -45,7 +45,8 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; @Table(name = "TRM_CONCEPT", uniqueConstraints = { @UniqueConstraint(name = "IDX_CONCEPT_CS_CODE", columnNames = {"CODESYSTEM_PID", "CODE"}) }, indexes = { - @Index(name = "IDX_CONCEPT_INDEXSTATUS", columnList = "INDEX_STATUS") + @Index(name = "IDX_CONCEPT_INDEXSTATUS", columnList = "INDEX_STATUS"), + @Index(name = "IDX_CONCEPT_UPDATED", columnList = "CONCEPT_UPDATED") }) public class TermConcept implements Serializable { protected static final int MAX_DESC_LENGTH = 400; @@ -59,15 +60,15 @@ public class TermConcept implements Serializable { @Column(name = "CODE", length = 100, nullable = false) @Fields({@Field(name = "myCode", index = org.hibernate.search.annotations.Index.YES, store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = "exactAnalyzer")),}) private String myCode; - + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "CONCEPT_UPDATED", nullable = true) + private Date myUpdated; @ManyToOne() @JoinColumn(name = "CODESYSTEM_PID", referencedColumnName = "PID", foreignKey = @ForeignKey(name = "FK_CONCEPT_PID_CS_PID")) private TermCodeSystemVersion myCodeSystem; - @Column(name = "CODESYSTEM_PID", insertable = false, updatable = false) @Fields({@Field(name = "myCodeSystemVersionPid")}) private long myCodeSystemVersionPid; - @Column(name = "DISPLAY", length = MAX_DESC_LENGTH, nullable = true) @Fields({ @Field(name = "myDisplay", index = org.hibernate.search.annotations.Index.YES, store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = "standardAnalyzer")), @@ -76,15 +77,12 @@ public class TermConcept implements Serializable { @Field(name = "myDisplayPhonetic", index = org.hibernate.search.annotations.Index.YES, store = Store.NO, analyze = Analyze.YES, analyzer = @Analyzer(definition = "autocompletePhoneticAnalyzer")) }) private String myDisplay; - - @OneToMany(mappedBy = "myConcept", orphanRemoval = true) - @Field + @OneToMany(mappedBy = "myConcept", orphanRemoval = false) + @Field(name = "PROPmyProperties", analyzer = @Analyzer(definition = "termConceptPropertyAnalyzer")) @FieldBridge(impl = TermConceptPropertyFieldBridge.class) private Collection myProperties; - - @OneToMany(mappedBy = "myConcept", orphanRemoval = true) + @OneToMany(mappedBy = "myConcept", orphanRemoval = false) private Collection myDesignations; - @Id() @SequenceGenerator(name = "SEQ_CONCEPT_PID", sequenceName = "SEQ_CONCEPT_PID") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_CONCEPT_PID") @@ -92,18 +90,17 @@ public class TermConcept implements Serializable { private Long myId; @Column(name = "INDEX_STATUS", nullable = true) private Long myIndexStatus; - @Transient @Field(name = "myParentPids", index = org.hibernate.search.annotations.Index.YES, store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = "conceptParentPidsAnalyzer")) + @Lob + @Column(name="PARENT_PIDS", nullable = true) private String myParentPids; @OneToMany(cascade = {}, fetch = FetchType.LAZY, mappedBy = "myChild") private Collection myParents; @Column(name = "CODE_SEQUENCE", nullable = true) private Integer mySequence; - public TermConcept() { super(); } - public TermConcept(TermCodeSystemVersion theCs, String theCode) { setCodeSystemVersion(theCs); setCode(theCode); @@ -130,6 +127,7 @@ public class TermConcept implements Serializable { public TermConceptDesignation addDesignation() { TermConceptDesignation designation = new TermConceptDesignation(); designation.setConcept(this); + designation.setCodeSystemVersion(myCodeSystem); getDesignations().add(designation); return designation; } @@ -139,6 +137,7 @@ public class TermConcept implements Serializable { TermConceptProperty property = new TermConceptProperty(); property.setConcept(this); + property.setCodeSystemVersion(myCodeSystem); property.setType(thePropertyType); property.setKey(thePropertyName); property.setValue(thePropertyValue); @@ -294,6 +293,14 @@ public class TermConcept implements Serializable { return null; } + public Date getUpdated() { + return myUpdated; + } + + public void setUpdated(Date theUpdated) { + myUpdated = theUpdated; + } + @Override public int hashCode() { HashCodeBuilder b = new HashCodeBuilder(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java index 0bdaa4ad492..bd99a8c3196 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java @@ -48,6 +48,14 @@ public class TermConceptDesignation implements Serializable { private String myUseDisplay; @Column(name = "VAL", length = 500, nullable = false) private String myValue; + /** + * TODO: Make this non-null + * + * @since 3.5.0 + */ + @ManyToOne + @JoinColumn(name = "CS_VER_PID", nullable = true, referencedColumnName = "PID", foreignKey = @ForeignKey(name = "FK_CONCEPTDESIG_CSV")) + private TermCodeSystemVersion myCodeSystemVersion; public String getLanguage() { return myLanguage; @@ -94,6 +102,11 @@ public class TermConceptDesignation implements Serializable { return this; } + public TermConceptDesignation setCodeSystemVersion(TermCodeSystemVersion theCodeSystemVersion) { + myCodeSystemVersion = theCodeSystemVersion; + return this; + } + public TermConceptDesignation setConcept(TermConcept theConcept) { myConcept = theConcept; return this; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java index 71f8c985d9d..e5aabc32a43 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java @@ -38,6 +38,14 @@ public class TermConceptProperty implements Serializable { @ManyToOne @JoinColumn(name = "CONCEPT_PID", referencedColumnName = "PID", foreignKey = @ForeignKey(name = "FK_CONCEPTPROP_CONCEPT")) private TermConcept myConcept; + /** + * TODO: Make this non-null + * + * @since 3.5.0 + */ + @ManyToOne + @JoinColumn(name = "CS_VER_PID", nullable = true, referencedColumnName = "PID", foreignKey = @ForeignKey(name = "FK_CONCEPTPROP_CSV")) + private TermCodeSystemVersion myCodeSystemVersion; @Id() @SequenceGenerator(name = "SEQ_CONCEPT_PROP_PID", sequenceName = "SEQ_CONCEPT_PROP_PID") @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_CONCEPT_PROP_PID") @@ -124,6 +132,11 @@ public class TermConceptProperty implements Serializable { myValue = theValue; } + public TermConceptProperty setCodeSystemVersion(TermCodeSystemVersion theCodeSystemVersion) { + myCodeSystemVersion = theCodeSystemVersion; + return this; + } + public void setConcept(TermConcept theConcept) { myConcept = theConcept; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptPropertyFieldBridge.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptPropertyFieldBridge.java index aacf7e28bbd..72641bdce07 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptPropertyFieldBridge.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptPropertyFieldBridge.java @@ -21,18 +21,22 @@ package ca.uhn.fhir.jpa.entity; */ import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; import org.hibernate.search.bridge.FieldBridge; import org.hibernate.search.bridge.LuceneOptions; import org.hibernate.search.bridge.StringBridge; import java.util.Collection; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + /** * Allows hibernate search to index individual concepts' properties */ public class TermConceptPropertyFieldBridge implements FieldBridge, StringBridge { - public static final String PROP_PREFIX = "PROP__"; + public static final String CONCEPT_FIELD_PROPERTY_PREFIX = "PROP"; /** * Constructor @@ -48,15 +52,17 @@ public class TermConceptPropertyFieldBridge implements FieldBridge, StringBridge @Override public void set(String theName, Object theValue, Document theDocument, LuceneOptions theLuceneOptions) { + @SuppressWarnings("unchecked") Collection properties = (Collection) theValue; + if (properties != null) { for (TermConceptProperty next : properties) { - String propValue = next.getKey() + "=" + next.getValue(); - theLuceneOptions.addFieldToDocument(theName, propValue, theDocument); + theDocument.add(new StringField(CONCEPT_FIELD_PROPERTY_PREFIX + next.getKey(), next.getValue(), Field.Store.YES)); if (next.getType() == TermConceptPropertyTypeEnum.CODING) { - propValue = next.getKey() + "=" + next.getDisplay(); - theLuceneOptions.addFieldToDocument(theName, propValue, theDocument); + if (isNotBlank(next.getDisplay())) { + theDocument.add(new StringField(CONCEPT_FIELD_PROPERTY_PREFIX + next.getKey(), next.getDisplay(), Field.Store.YES)); + } } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderMessageHeaderDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderMessageHeaderDstu2.java new file mode 100644 index 00000000000..c4eaffa40f0 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderMessageHeaderDstu2.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.jpa.provider; + +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoMessageHeader; +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.dstu2.resource.Bundle; +import ca.uhn.fhir.model.dstu2.resource.MessageHeader; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.instance.model.api.IBaseBundle; + +import javax.servlet.http.HttpServletRequest; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class BaseJpaResourceProviderMessageHeaderDstu2 extends JpaResourceProviderDstu2 { + + + /** + * /MessageHeader/$process-message + */ + @Operation(name = JpaConstants.OPERATION_PROCESS_MESSAGE, idempotent = false) + public IBaseBundle processMessage( + HttpServletRequest theServletRequest, + RequestDetails theRequestDetails, + + @OperationParam(name = "content", min = 1, max = 1) + @Description(formalDefinition = "The message to process (or, if using asynchronous messaging, it may be a response message to accept)") + Bundle theMessageToProcess + ) { + + startRequest(theServletRequest); + try { + return ((IFhirResourceDaoMessageHeader) getDao()).messageHeaderProcessMessage(theRequestDetails, theMessageToProcess); + } finally { + endRequest(theServletRequest); + } + + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderMessageHeaderDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderMessageHeaderDstu3.java new file mode 100644 index 00000000000..ee23124e60e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/BaseJpaResourceProviderMessageHeaderDstu3.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.jpa.provider.dstu3; + +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoMessageHeader; +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.MessageHeader; +import org.hl7.fhir.instance.model.api.IBaseBundle; + +import javax.servlet.http.HttpServletRequest; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class BaseJpaResourceProviderMessageHeaderDstu3 extends JpaResourceProviderDstu3 { + + + /** + * /MessageHeader/$process-message + */ + @Operation(name = JpaConstants.OPERATION_PROCESS_MESSAGE, idempotent = false) + public IBaseBundle processMessage( + HttpServletRequest theServletRequest, + RequestDetails theRequestDetails, + + @OperationParam(name = "content", min = 1, max = 1) + @Description(formalDefinition = "The message to process (or, if using asynchronous messaging, it may be a response message to accept)") + Bundle theMessageToProcess + ) { + + startRequest(theServletRequest); + try { + return ((IFhirResourceDaoMessageHeader) getDao()).messageHeaderProcessMessage(theRequestDetails, theMessageToProcess); + } finally { + endRequest(theServletRequest); + } + + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderMessageHeaderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderMessageHeaderR4.java new file mode 100644 index 00000000000..6934d9e033e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/BaseJpaResourceProviderMessageHeaderR4.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoMessageHeader; +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.MessageHeader; + +import javax.servlet.http.HttpServletRequest; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public class BaseJpaResourceProviderMessageHeaderR4 extends JpaResourceProviderR4 { + + + /** + * /MessageHeader/$process-message + */ + @Operation(name = JpaConstants.OPERATION_PROCESS_MESSAGE, idempotent = false) + public IBaseBundle processMessage( + HttpServletRequest theServletRequest, + RequestDetails theRequestDetails, + + @OperationParam(name = "content", min = 1, max = 1) + @Description(formalDefinition = "The message to process (or, if using asynchronous messaging, it may be a response message to accept)") + Bundle theMessageToProcess + ) { + + startRequest(theServletRequest); + try { + return ((IFhirResourceDaoMessageHeader) getDao()).messageHeaderProcessMessage(theRequestDetails, theMessageToProcess); + } finally { + endRequest(theServletRequest); + } + + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/LuceneSearchMappingFactory.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/LuceneSearchMappingFactory.java index a47fb022e75..e94da64629a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/LuceneSearchMappingFactory.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/LuceneSearchMappingFactory.java @@ -20,9 +20,7 @@ package ca.uhn.fhir.jpa.search; * #L% */ -import org.apache.lucene.analysis.core.LowerCaseFilterFactory; -import org.apache.lucene.analysis.core.StopFilterFactory; -import org.apache.lucene.analysis.core.WhitespaceTokenizerFactory; +import org.apache.lucene.analysis.core.*; import org.apache.lucene.analysis.miscellaneous.WordDelimiterFilterFactory; import org.apache.lucene.analysis.ngram.EdgeNGramFilterFactory; import org.apache.lucene.analysis.ngram.NGramFilterFactory; @@ -65,8 +63,9 @@ public class LuceneSearchMappingFactory { .param("maxGramSize", "20") .analyzerDef("standardAnalyzer", StandardTokenizerFactory.class) .filter(LowerCaseFilterFactory.class) - .analyzerDef("exactAnalyzer", StandardTokenizerFactory.class) - .analyzerDef("conceptParentPidsAnalyzer", WhitespaceTokenizerFactory.class); + .analyzerDef("exactAnalyzer", KeywordTokenizerFactory.class) + .analyzerDef("conceptParentPidsAnalyzer", WhitespaceTokenizerFactory.class) + .analyzerDef("termConceptPropertyAnalyzer", WhitespaceTokenizerFactory.class); return mapping; } 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 031c72c5006..30c83c23b4f 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 @@ -178,9 +178,9 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { @Override public List doInTransaction(TransactionStatus theStatus) { final List resultPids = new ArrayList(); - Page searchResults = mySearchResultDao.findWithSearchUuid(foundSearch, page); - for (SearchResult next : searchResults) { - resultPids.add(next.getResourcePid()); + Page searchResultPids = mySearchResultDao.findWithSearchUuid(foundSearch, page); + for (Long next : searchResultPids) { + resultPids.add(next); } return resultPids; } 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 db513303408..551aac0338a 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,14 +20,12 @@ package ca.uhn.fhir.jpa.sp; * #L% */ -import java.util.Map; - import ca.uhn.fhir.jpa.entity.ResourceTable; +import java.util.Map; + public interface ISearchParamPresenceSvc { void updatePresence(ResourceTable theResource, Map theParamNameToPresence); - void flushCachesForUnitTest(); - } 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 b1e5c09b53a..7ffeef7fa45 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 @@ -20,29 +20,17 @@ package ca.uhn.fhir.jpa.sp; * #L% */ -import java.util.*; -import java.util.Map.Entry; -import java.util.concurrent.ConcurrentHashMap; - import ca.uhn.fhir.jpa.dao.DaoConfig; -import org.apache.commons.lang3.tuple.Pair; -import org.springframework.beans.factory.annotation.Autowired; - -import ca.uhn.fhir.jpa.dao.data.ISearchParamDao; import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao; import ca.uhn.fhir.jpa.entity.ResourceTable; -import ca.uhn.fhir.jpa.entity.SearchParam; import ca.uhn.fhir.jpa.entity.SearchParamPresent; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.*; +import java.util.Map.Entry; public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamPresenceSvcImpl.class); - - private Map, SearchParam> myResourceTypeToSearchParamToEntity = new ConcurrentHashMap, SearchParam>(); - - @Autowired - private ISearchParamDao mySearchParamDao; - @Autowired private ISearchParamPresentDao mySearchParamPresentDao; @@ -55,62 +43,48 @@ public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc { return; } - Map presenceMap = new HashMap(theParamNameToPresence); - List entitiesToSave = new ArrayList(); - List entitiesToDelete = new ArrayList(); + Map presenceMap = new HashMap<>(theParamNameToPresence); + // Find existing entries Collection existing; existing = mySearchParamPresentDao.findAllForResource(theResource); - + Map existingHashToPresence = new HashMap<>(); for (SearchParamPresent nextExistingEntity : existing) { - String nextSearchParamName = nextExistingEntity.getSearchParam().getParamName(); - Boolean existingValue = presenceMap.remove(nextSearchParamName); - if (existingValue == null) { - entitiesToDelete.add(nextExistingEntity); - } else if (existingValue.booleanValue() == nextExistingEntity.isPresent()) { - ourLog.trace("No change for search param {}", nextSearchParamName); - } else { - nextExistingEntity.setPresent(existingValue); - entitiesToSave.add(nextExistingEntity); - } + existingHashToPresence.put(nextExistingEntity.getHashPresence(), nextExistingEntity); } + // Find newly wanted set of entries + Map newHashToPresence = new HashMap<>(); for (Entry next : presenceMap.entrySet()) { - String resourceType = theResource.getResourceType(); String paramName = next.getKey(); - Pair key = Pair.of(resourceType, paramName); - - SearchParam searchParam = myResourceTypeToSearchParamToEntity.get(key); - if (searchParam == null) { - searchParam = mySearchParamDao.findForResource(resourceType, paramName); - if (searchParam != null) { - myResourceTypeToSearchParamToEntity.put(key, searchParam); - } else { - searchParam = new SearchParam(); - searchParam.setResourceName(resourceType); - searchParam.setParamName(paramName); - searchParam = mySearchParamDao.save(searchParam); - ourLog.info("Added search param {} with pid {}", paramName, searchParam.getId()); - // Don't add the newly saved entity to the map in case the save fails - } - } SearchParamPresent present = new SearchParamPresent(); present.setResource(theResource); - present.setSearchParam(searchParam); + present.setParamName(paramName); present.setPresent(next.getValue()); - entitiesToSave.add(present); + present.calculateHashes(); + newHashToPresence.put(present.getHashPresence(), present); } - mySearchParamPresentDao.deleteInBatch(entitiesToDelete); - mySearchParamPresentDao.saveAll(entitiesToSave); + // Delete any that should be deleted + List toDelete = new ArrayList<>(); + for (Entry nextEntry : existingHashToPresence.entrySet()) { + if (newHashToPresence.containsKey(nextEntry.getKey()) == false) { + toDelete.add(nextEntry.getValue()); + } + } + mySearchParamPresentDao.deleteInBatch(toDelete); - } + // Add any that should be added + List toAdd = new ArrayList<>(); + for (Entry nextEntry : newHashToPresence.entrySet()) { + if (existingHashToPresence.containsKey(nextEntry.getKey()) == false) { + toAdd.add(nextEntry.getValue()); + } + } + mySearchParamPresentDao.saveAll(toAdd); - @Override - public void flushCachesForUnitTest() { - myResourceTypeToSearchParamToEntity.clear(); } } 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 c9c5992c09e..61d6f45cc8e 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 @@ -40,15 +40,20 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.collect.ArrayListMultimap; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; -import org.apache.lucene.search.Query; +import org.apache.lucene.index.Term; +import org.apache.lucene.queries.TermsQuery; +import org.apache.lucene.search.*; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; 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.hibernate.search.query.dsl.TermMatchingContext; +import org.hibernate.search.query.dsl.TermTermination; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.CodeSystem; @@ -61,6 +66,8 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +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.TransactionStatus; @@ -77,6 +84,7 @@ import javax.persistence.TypedQuery; import javax.persistence.criteria.*; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -131,10 +139,14 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, private int myFetchSize = DEFAULT_FETCH_SIZE; private ApplicationContext myApplicationContext; - private void addCodeIfNotAlreadyAdded(String theCodeSystem, ValueSet.ValueSetExpansionComponent theExpansionComponent, Set theAddedCodes, TermConcept theConcept) { - if (theAddedCodes.add(theConcept.getCode())) { + /** + * @param theAdd If true, add the code. If false, remove the code. + */ + private void addCodeIfNotAlreadyAdded(String theCodeSystem, ValueSet.ValueSetExpansionComponent theExpansionComponent, Set theAddedCodes, TermConcept theConcept, boolean theAdd) { + String code = theConcept.getCode(); + if (theAdd && theAddedCodes.add(code)) { ValueSet.ValueSetExpansionContainsComponent contains = theExpansionComponent.addContains(); - contains.setCode(theConcept.getCode()); + contains.setCode(code); contains.setSystem(theCodeSystem); contains.setDisplay(theConcept.getDisplay()); for (TermConceptDesignation nextDesignation : theConcept.getDesignations()) { @@ -147,18 +159,24 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, .setDisplay(nextDesignation.getUseDisplay()); } } + + if (!theAdd && theAddedCodes.remove(code)) { + removeCodeFromExpansion(theCodeSystem, code, theExpansionComponent); + } } - private void addConceptsToList(ValueSet.ValueSetExpansionComponent theExpansionComponent, Set theAddedCodes, String theSystem, List theConcept) { + private void addConceptsToList(ValueSet.ValueSetExpansionComponent theExpansionComponent, Set theAddedCodes, String theSystem, List theConcept, boolean theAdd) { for (CodeSystem.ConceptDefinitionComponent next : theConcept) { - if (!theAddedCodes.contains(next.getCode())) { - theAddedCodes.add(next.getCode()); + if (theAdd && theAddedCodes.add(next.getCode())) { ValueSet.ValueSetExpansionContainsComponent contains = theExpansionComponent.addContains(); contains.setCode(next.getCode()); contains.setSystem(theSystem); contains.setDisplay(next.getDisplay()); } - addConceptsToList(theExpansionComponent, theAddedCodes, theSystem, next.getConcept()); + if (!theAdd && theAddedCodes.remove(next.getCode())) { + removeCodeFromExpansion(theSystem, next.getCode(), theExpansionComponent); + } + addConceptsToList(theExpansionComponent, theAddedCodes, theSystem, next.getConcept(), theAdd); } } @@ -250,21 +268,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, int i = 0; for (TermCodeSystemVersion next : myCodeSystemVersionDao.findByCodeSystemResource(theCodeSystem.getPid())) { - myConceptParentChildLinkDao.deleteByCodeSystemVersion(next.getPid()); - for (TermConcept nextConcept : myConceptDao.findByCodeSystemVersion(next.getPid())) { - myConceptPropertyDao.deleteAll(nextConcept.getProperties()); - myConceptDesignationDao.deleteAll(nextConcept.getDesignations()); - myConceptDao.delete(nextConcept); - } - if (next.getCodeSystem().getCurrentVersion() == next) { - next.getCodeSystem().setCurrentVersion(null); - myCodeSystemDao.save(next.getCodeSystem()); - } - myCodeSystemVersionDao.delete(next); - - if (i++ % 1000 == 0) { - myEntityManager.flush(); - } + deleteCodeSystemVersion(next.getPid()); } myCodeSystemVersionDao.deleteForCodeSystem(theCodeSystem); myCodeSystemDao.delete(theCodeSystem); @@ -272,6 +276,119 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, myEntityManager.flush(); } + public void deleteCodeSystemVersion(final Long theCodeSystemVersionPid) { + ourLog.info(" * Deleting code system version {}", theCodeSystemVersionPid); + + PageRequest page1000 = PageRequest.of(0, 1000); + + // Parent/Child links + { + String descriptor = "parent/child links"; + Supplier> loader = () -> myConceptParentChildLinkDao.findByCodeSystemVersion(page1000, theCodeSystemVersionPid); + Supplier counter = () -> myConceptParentChildLinkDao.countByCodeSystemVersion(theCodeSystemVersionPid); + doDelete(descriptor, loader, counter, myConceptParentChildLinkDao); + } + + // Properties + { + String descriptor = "concept properties"; + Supplier> loader = () -> myConceptPropertyDao.findByCodeSystemVersion(page1000, theCodeSystemVersionPid); + Supplier counter = () -> myConceptPropertyDao.countByCodeSystemVersion(theCodeSystemVersionPid); + doDelete(descriptor, loader, counter, myConceptPropertyDao); + } + + // Designations + { + String descriptor = "concept designations"; + Supplier> loader = () -> myConceptDesignationDao.findByCodeSystemVersion(page1000, theCodeSystemVersionPid); + Supplier counter = () -> myConceptDesignationDao.countByCodeSystemVersion(theCodeSystemVersionPid); + doDelete(descriptor, loader, counter, myConceptDesignationDao); + } + + // Concepts + { + String descriptor = "concepts"; + // For some reason, concepts are much slower to delete, so use a smaller batch size + PageRequest page100 = PageRequest.of(0, 100); + Supplier> loader = () -> myConceptDao.findByCodeSystemVersion(page100, theCodeSystemVersionPid); + Supplier counter = () -> myConceptDao.countByCodeSystemVersion(theCodeSystemVersionPid); + doDelete(descriptor, loader, counter, myConceptDao); + } + + Optional codeSystemOpt = myCodeSystemDao.findWithCodeSystemVersionAsCurrentVersion(theCodeSystemVersionPid); + if (codeSystemOpt.isPresent()) { + TermCodeSystem codeSystem = codeSystemOpt.get(); + ourLog.info(" * Removing code system version {} as current version of code system {}", theCodeSystemVersionPid, codeSystem.getPid()); + codeSystem.setCurrentVersion(null); + myCodeSystemDao.save(codeSystem); + } + + ourLog.info(" * Deleting code system version"); + myCodeSystemVersionDao.deleteById(theCodeSystemVersionPid); + + } + + public void deleteConceptMap(ResourceTable theResourceTable) { + // Get existing entity so it can be deleted. + Optional optionalExistingTermConceptMapById = myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId()); + + if (optionalExistingTermConceptMapById.isPresent()) { + TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get(); + + ourLog.info("Deleting existing TermConceptMap {} and its children...", existingTermConceptMap.getId()); + for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) { + + for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) { + + for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) { + + myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId()); + } + + myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId()); + } + + myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId()); + } + + myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId()); + ourLog.info("Done deleting existing TermConceptMap {} and its children.", existingTermConceptMap.getId()); + + ourLog.info("Flushing..."); + myConceptMapGroupElementTargetDao.flush(); + myConceptMapGroupElementDao.flush(); + myConceptMapGroupDao.flush(); + myConceptMapDao.flush(); + ourLog.info("Done flushing."); + } + } + + @Override + @Transactional + public void deleteConceptMapAndChildren(ResourceTable theResourceTable) { + deleteConceptMap(theResourceTable); + } + + private void doDelete(String theDescriptor, Supplier> theLoader, Supplier theCounter, JpaRepository theDao) { + int count; + ourLog.info(" * Deleting {}", theDescriptor); + int totalCount = theCounter.get(); + StopWatch sw = new StopWatch(); + count = 0; + while (true) { + Slice link = theLoader.get(); + if (link.hasContent() == false) { + break; + } + + theDao.deleteInBatch(link); + + count += link.getNumberOfElements(); + ourLog.info(" * {} {} deleted - {}/sec - ETA: {}", count, theDescriptor, sw.formatThroughput(count, TimeUnit.SECONDS), sw.getEstimatedTimeRemaining(count, totalCount)); + } + theDao.flush(); + } + private int ensureParentsSaved(Collection theParents) { ourLog.trace("Checking {} parents", theParents.size()); int retVal = 0; @@ -281,6 +398,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, TermConcept nextParent = nextLink.getParent(); retVal += ensureParentsSaved(nextParent.getParents()); if (nextParent.getId() == null) { + nextParent.setUpdated(new Date()); myConceptDao.saveAndFlush(nextParent); retVal++; ourLog.debug("Saved parent code {} and got id {}", nextParent.getCode(), nextParent.getId()); @@ -296,133 +414,17 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, public ValueSet expandValueSet(ValueSet theValueSetToExpand) { ValueSet.ValueSetExpansionComponent expansionComponent = new ValueSet.ValueSetExpansionComponent(); Set addedCodes = new HashSet<>(); - boolean haveIncludeCriteria = false; + // Handle includes for (ValueSet.ConceptSetComponent include : theValueSetToExpand.getCompose().getInclude()) { - String system = include.getSystem(); - if (isNotBlank(system)) { - ourLog.info("Starting expansion around code system: {}", system); + boolean add = true; + expandValueSetHandleIncludeOrExclude(expansionComponent, addedCodes, include, add); + } - TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(system); - if (cs != null) { - TermCodeSystemVersion csv = cs.getCurrentVersion(); - - /* - * Include Concepts - */ - for (ValueSet.ConceptReferenceComponent next : include.getConcept()) { - String nextCode = next.getCode(); - if (isNotBlank(nextCode) && !addedCodes.contains(nextCode)) { - haveIncludeCriteria = true; - TermConcept code = findCode(system, nextCode); - if (code != null) { - addCodeIfNotAlreadyAdded(system, expansionComponent, addedCodes, code); - } - } - } - - /* - * Filters - */ - - if (include.getFilter().size() > 0) { - haveIncludeCriteria = true; - - FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager); - QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(TermConcept.class).get(); - BooleanJunction bool = qb.bool(); - - bool.must(qb.keyword().onField("myCodeSystemVersionPid").matching(csv.getPid()).createQuery()); - - for (ValueSet.ConceptSetFilterComponent nextFilter : include.getFilter()) { - if (isBlank(nextFilter.getValue()) && nextFilter.getOp() == null && isBlank(nextFilter.getProperty())) { - continue; - } - - if (isBlank(nextFilter.getValue()) || nextFilter.getOp() == null || isBlank(nextFilter.getProperty())) { - throw new InvalidRequestException("Invalid filter, must have fields populated: property op value"); - } - - - if (nextFilter.getProperty().equals("display:exact") && nextFilter.getOp() == ValueSet.FilterOperator.EQUAL) { - addDisplayFilterExact(qb, bool, nextFilter); - } else if ("display".equals(nextFilter.getProperty()) && nextFilter.getOp() == ValueSet.FilterOperator.EQUAL) { - if (nextFilter.getValue().trim().contains(" ")) { - addDisplayFilterExact(qb, bool, nextFilter); - } else { - addDisplayFilterInexact(qb, bool, nextFilter); - } - } else if ((nextFilter.getProperty().equals("concept") || nextFilter.getProperty().equals("code")) && nextFilter.getOp() == ValueSet.FilterOperator.ISA) { - - TermConcept code = findCode(system, nextFilter.getValue()); - if (code == null) { - throw new InvalidRequestException("Invalid filter criteria - code does not exist: {" + system + "}" + nextFilter.getValue()); - } - - ourLog.info(" * Filtering on codes with a parent of {}/{}/{}", code.getId(), code.getCode(), code.getDisplay()); - bool.must(qb.keyword().onField("myParentPids").matching("" + code.getId()).createQuery()); - - } else { - - bool.must(qb.phrase().onField("myProperties").sentence(nextFilter.getProperty() + "=" + nextFilter.getValue()).createQuery()); - - } - } - - Query luceneQuery = bool.createQuery(); - FullTextQuery jpaQuery = em.createFullTextQuery(luceneQuery, TermConcept.class); - jpaQuery.setMaxResults(1000); - - StopWatch sw = new StopWatch(); - - @SuppressWarnings("unchecked") - List result = jpaQuery.getResultList(); - - ourLog.info("Expansion completed in {}ms", sw.getMillis()); - - for (TermConcept nextConcept : result) { - addCodeIfNotAlreadyAdded(system, expansionComponent, addedCodes, nextConcept); - } - - expansionComponent.setTotal(jpaQuery.getResultSize()); - } - - if (!haveIncludeCriteria) { - List allCodes = findCodes(system); - for (TermConcept nextConcept : allCodes) { - addCodeIfNotAlreadyAdded(system, expansionComponent, addedCodes, nextConcept); - } - } - - } else { - // No codesystem matching the URL found in the database - - CodeSystem codeSystemFromContext = getCodeSystemFromContext(system); - if (codeSystemFromContext == null) { - throw new InvalidRequestException("Unknown code system: " + system); - } - - if (include.getConcept().isEmpty() == false) { - for (ValueSet.ConceptReferenceComponent next : include.getConcept()) { - String nextCode = next.getCode(); - if (isNotBlank(nextCode) && !addedCodes.contains(nextCode)) { - CodeSystem.ConceptDefinitionComponent code = findCode(codeSystemFromContext.getConcept(), nextCode); - if (code != null) { - addedCodes.add(nextCode); - ValueSet.ValueSetExpansionContainsComponent contains = expansionComponent.addContains(); - contains.setCode(nextCode); - contains.setSystem(system); - contains.setDisplay(code.getDisplay()); - } - } - } - } else { - List concept = codeSystemFromContext.getConcept(); - addConceptsToList(expansionComponent, addedCodes, system, concept); - } - - } - } + // Handle excludes + for (ValueSet.ConceptSetComponent include : theValueSetToExpand.getCompose().getExclude()) { + boolean add = false; + expandValueSetHandleIncludeOrExclude(expansionComponent, addedCodes, include, add); } ValueSet valueSet = new ValueSet(); @@ -443,6 +445,173 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, return retVal; } + public void expandValueSetHandleIncludeOrExclude(ValueSet.ValueSetExpansionComponent theExpansionComponent, Set theAddedCodes, ValueSet.ConceptSetComponent include, boolean theAdd) { + String system = include.getSystem(); + if (isNotBlank(system)) { + ourLog.info("Starting expansion around code system: {}", system); + + TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(system); + if (cs != null) { + TermCodeSystemVersion csv = cs.getCurrentVersion(); + FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager); + QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(TermConcept.class).get(); + BooleanJunction bool = qb.bool(); + + bool.must(qb.keyword().onField("myCodeSystemVersionPid").matching(csv.getPid()).createQuery()); + + /* + * Filters + */ + + if (include.getFilter().size() > 0) { + + for (ValueSet.ConceptSetFilterComponent nextFilter : include.getFilter()) { + if (isBlank(nextFilter.getValue()) && nextFilter.getOp() == null && isBlank(nextFilter.getProperty())) { + continue; + } + + if (isBlank(nextFilter.getValue()) || nextFilter.getOp() == null || isBlank(nextFilter.getProperty())) { + throw new InvalidRequestException("Invalid filter, must have fields populated: property op value"); + } + + + if (nextFilter.getProperty().equals("display:exact") && nextFilter.getOp() == ValueSet.FilterOperator.EQUAL) { + addDisplayFilterExact(qb, bool, nextFilter); + } else if ("display".equals(nextFilter.getProperty()) && nextFilter.getOp() == ValueSet.FilterOperator.EQUAL) { + if (nextFilter.getValue().trim().contains(" ")) { + addDisplayFilterExact(qb, bool, nextFilter); + } else { + addDisplayFilterInexact(qb, bool, nextFilter); + } + } else if (nextFilter.getProperty().equals("concept") || nextFilter.getProperty().equals("code")) { + + TermConcept code = findCode(system, nextFilter.getValue()); + if (code == null) { + throw new InvalidRequestException("Invalid filter criteria - code does not exist: {" + system + "}" + nextFilter.getValue()); + } + + if (nextFilter.getOp() == ValueSet.FilterOperator.ISA) { + ourLog.info(" * Filtering on codes with a parent of {}/{}/{}", code.getId(), code.getCode(), code.getDisplay()); + bool.must(qb.keyword().onField("myParentPids").matching("" + code.getId()).createQuery()); + } else { + throw new InvalidRequestException("Don't know how to handle op=" + nextFilter.getOp() + " on property " + nextFilter.getProperty()); + } + + } else { + + if (nextFilter.getOp() == ValueSet.FilterOperator.REGEX) { + + /* + * We treat the regex filter as a match on the regex + * anywhere in the property string. The spec does not + * say whether or not this is the right behaviour, but + * there are examples that seem to suggest that it is. + */ + String value = nextFilter.getValue(); + if (value.endsWith("$")) { + value = value.substring(0, value.length() - 1); + } else if (value.endsWith(".*") == false) { + value = value + ".*"; + } + if (value.startsWith("^") == false && value.startsWith(".*") == false) { + value = ".*" + value; + } else if (value.startsWith("^")) { + value = value.substring(1); + } + + Term term = new Term(TermConceptPropertyFieldBridge.CONCEPT_FIELD_PROPERTY_PREFIX + nextFilter.getProperty(), value); + RegexpQuery query = new RegexpQuery(term); + bool.must(query); + + } else { + + String value = nextFilter.getValue(); + Term term = new Term(TermConceptPropertyFieldBridge.CONCEPT_FIELD_PROPERTY_PREFIX + nextFilter.getProperty(), value); + bool.must(new TermsQuery(term)); + + } + + } + } + + } + + Query luceneQuery = bool.createQuery(); + + /* + * Include Concepts + */ + + List codes = include + .getConcept() + .stream() + .filter(Objects::nonNull) + .map(ValueSet.ConceptReferenceComponent::getCode) + .filter(StringUtils::isNotBlank) + .map(t->new Term("myCode", t)) + .collect(Collectors.toList()); + if (codes.size() > 0) { + MultiPhraseQuery query = new MultiPhraseQuery(); + query.add(codes.toArray(new Term[0])); + luceneQuery = new BooleanQuery.Builder() + .add(luceneQuery, BooleanClause.Occur.MUST) + .add(query, BooleanClause.Occur.MUST) + .build(); + } + + /* + * Execute the query + */ + + FullTextQuery jpaQuery = em.createFullTextQuery(luceneQuery, TermConcept.class); + jpaQuery.setMaxResults(1000); + + StopWatch sw = new StopWatch(); + + @SuppressWarnings("unchecked") + List result = jpaQuery.getResultList(); + + ourLog.info("Expansion completed in {}ms", sw.getMillis()); + + for (TermConcept nextConcept : result) { + addCodeIfNotAlreadyAdded(system, theExpansionComponent, theAddedCodes, nextConcept, theAdd); + } + + } else { + // No codesystem matching the URL found in the database + + CodeSystem codeSystemFromContext = getCodeSystemFromContext(system); + if (codeSystemFromContext == null) { + throw new InvalidRequestException("Unknown code system: " + system); + } + + if (include.getConcept().isEmpty() == false) { + for (ValueSet.ConceptReferenceComponent next : include.getConcept()) { + String nextCode = next.getCode(); + if (isNotBlank(nextCode) && !theAddedCodes.contains(nextCode)) { + CodeSystem.ConceptDefinitionComponent code = findCode(codeSystemFromContext.getConcept(), nextCode); + if (code != null) { + if (theAdd && theAddedCodes.add(nextCode)) { + ValueSet.ValueSetExpansionContainsComponent contains = theExpansionComponent.addContains(); + contains.setCode(nextCode); + contains.setSystem(system); + contains.setDisplay(code.getDisplay()); + } + if (!theAdd && theAddedCodes.remove(nextCode)) { + removeCodeFromExpansion(system, nextCode, theExpansionComponent); + } + } + } + } + } else { + List concept = codeSystemFromContext.getConcept(); + addConceptsToList(theExpansionComponent, theAddedCodes, system, concept, theAdd); + } + + } + } + } + private void fetchChildren(TermConcept theConcept, Set theSetToPopulate) { for (TermConceptParentChildLink nextChildLink : theConcept.getChildren()) { TermConcept nextChild = nextChildLink.getChild(); @@ -731,9 +900,11 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, for (TermConcept nextConcept : concepts) { - StringBuilder parentsBuilder = new StringBuilder(); - createParentsString(parentsBuilder, nextConcept.getId()); - nextConcept.setParentPids(parentsBuilder.toString()); + if (isBlank(nextConcept.getParentPidsAsString())) { + StringBuilder parentsBuilder = new StringBuilder(); + createParentsString(parentsBuilder, nextConcept.getId()); + nextConcept.setParentPids(parentsBuilder.toString()); + } saveConcept(nextConcept); count++; @@ -745,6 +916,14 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, } + private void removeCodeFromExpansion(String theCodeSystem, String theCode, ValueSet.ValueSetExpansionComponent theExpansionComponent) { + theExpansionComponent + .getContains() + .removeIf(t -> + theCodeSystem.equals(t.getSystem()) && + theCode.equals(t.getCode())); + } + private int saveConcept(TermConcept theConcept) { int retVal = 0; @@ -759,6 +938,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, if (theConcept.getId() == null || theConcept.getIndexStatus() == null) { retVal++; theConcept.setIndexStatus(BaseHapiFhirDao.INDEX_STATUS_INDEXED); + theConcept.setUpdated(new Date()); myConceptDao.save(theConcept); for (TermConceptProperty next : theConcept.getProperties()) { @@ -788,15 +968,16 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, return; } else if (myDeferredConcepts.isEmpty() && myConceptLinksToSaveLater.isEmpty()) { processReindexing(); - return; } TransactionTemplate tt = new TransactionTemplate(myTransactionMgr); tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); - tt.execute(t -> { - processDeferredConcepts(); - return null; - }); + if (!myDeferredConcepts.isEmpty() || !myConceptLinksToSaveLater.isEmpty()) { + tt.execute(t -> { + processDeferredConcepts(); + return null; + }); + } if (myDeferredValueSets.size() > 0) { tt.execute(t -> { @@ -847,20 +1028,12 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, ourLog.info("Deleting old code system versions"); for (TermCodeSystemVersion next : existing) { - ourLog.info(" * Deleting code system version {}", next.getPid()); - myConceptParentChildLinkDao.deleteByCodeSystemVersion(next.getPid()); - for (TermConcept nextConcept : myConceptDao.findByCodeSystemVersion(next.getPid())) { - myConceptPropertyDao.deleteAll(nextConcept.getProperties()); - myConceptDao.delete(nextConcept); - } + Long codeSystemVersionPid = next.getPid(); + deleteCodeSystemVersion(codeSystemVersionPid); } ourLog.info("Flushing..."); - - myConceptParentChildLinkDao.flush(); - myConceptPropertyDao.flush(); myConceptDao.flush(); - ourLog.info("Done flushing"); /* @@ -905,7 +1078,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, codeSystem.setCurrentVersion(theCodeSystemVersion); codeSystem = myCodeSystemDao.saveAndFlush(codeSystem); - ourLog.info("Setting codesystemversion on {} concepts...", totalCodeCount); + ourLog.info("Setting CodeSystemVersion[{}] on {} concepts...", codeSystem.getPid(), totalCodeCount); for (TermConcept next : theCodeSystemVersion.getConcepts()) { populateVersion(next, codeSystemVersion); @@ -963,42 +1136,10 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, termConceptMap.setResource(theResourceTable); termConceptMap.setUrl(theConceptMap.getUrl()); - // Get existing entity so it can be deleted. - Optional optionalExistingTermConceptMapById = myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId()); - /* * For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions. */ - - if (optionalExistingTermConceptMapById.isPresent()) { - TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get(); - - ourLog.info("Deleting existing TermConceptMap {} and its children...", existingTermConceptMap.getId()); - for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) { - - for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) { - - for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) { - - myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId()); - } - - myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId()); - } - - myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId()); - } - - myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId()); - ourLog.info("Done deleting existing TermConceptMap {} and its children.", existingTermConceptMap.getId()); - - ourLog.info("Flushing..."); - myConceptMapGroupElementTargetDao.flush(); - myConceptMapGroupElementDao.flush(); - myConceptMapGroupDao.flush(); - myConceptMapDao.flush(); - ourLog.info("Done flushing."); - } + deleteConceptMap(theResourceTable); /* * Do the upload. @@ -1019,6 +1160,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, throw new InternalErrorException(fe); } myConceptMapDao.save(termConceptMap); + int codesSaved = 0; if (theConceptMap.hasGroup()) { TermConceptMapGroup termConceptMapGroup; @@ -1054,7 +1196,12 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, termConceptMapGroupElementTarget.setCode(target.getCode()); termConceptMapGroupElementTarget.setDisplay(target.getDisplay()); termConceptMapGroupElementTarget.setEquivalence(target.getEquivalence()); - myConceptMapGroupElementTargetDao.saveAndFlush(termConceptMapGroupElementTarget); + myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget); + + if (codesSaved++ % 250 == 0) { + ourLog.info("Have saved {} codes in conceptmap", codesSaved); + myConceptMapGroupElementTargetDao.flush(); + } } } } 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 dbe52226f78..be8a1c81824 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 @@ -68,6 +68,8 @@ public interface IHapiTerminologySvc { */ IIdType storeNewCodeSystemVersion(org.hl7.fhir.r4.model.CodeSystem theCodeSystemResource, TermCodeSystemVersion theCodeSystemVersion, RequestDetails theRequestDetails, List theValueSets, List theConceptMaps); + void deleteConceptMapAndChildren(ResourceTable theResourceTable); + void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap); boolean supportsSystem(String theCodeSystem); 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 0f53d668832..0f647f57b4c 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 @@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.term.snomedct.SctHandlerRelationship; import ca.uhn.fhir.jpa.util.Counter; import ca.uhn.fhir.rest.api.server.RequestDetails; 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 com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; @@ -62,23 +63,27 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { public static final String SCT_FILE_CONCEPT = "Terminology/sct2_Concept_Full_"; public static final String SCT_FILE_DESCRIPTION = "Terminology/sct2_Description_Full-en"; public static final String SCT_FILE_RELATIONSHIP = "Terminology/sct2_Relationship_Full"; - public static final String LOINC_ANSWERLIST_FILE = "AnswerList_Beta_1.csv"; - public static final String LOINC_ANSWERLIST_LINK_FILE = "LoincAnswerListLink_Beta_1.csv"; + public static final String LOINC_ANSWERLIST_FILE = "AnswerList.csv"; + 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_HIERARCHY_FILE = "MULTI-AXIAL_HIERARCHY.CSV"; - public static final String LOINC_PART_FILE = "Part_Beta_1.csv"; - public static final String LOINC_PART_LINK_FILE = "LoincPartLink_Beta_1.csv"; - public static final String LOINC_PART_RELATED_CODE_MAPPING_FILE = "PartRelatedCodeMapping_Beta_1.csv"; + public static final String LOINC_FILE = "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"; + public static final String LOINC_PART_RELATED_CODE_MAPPING_FILE = "PartRelatedCodeMapping.csv"; public static final String LOINC_RSNA_PLAYBOOK_FILE = "LoincRsnaRadiologyPlaybook.csv"; - public static final String LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE = "Top2000CommonLabResultsUS.csv"; - public static final String LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE = "Top2000CommonLabResultsSI.csv"; + public static final String LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE = "Top2000CommonLabResultsUs.csv"; + public static final String LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE = "Top2000CommonLabResultsSi.csv"; public static final String LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE = "LoincUniversalLabOrdersValueSet.csv"; public static final String LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_CSV = "LoincIeeeMedicalDeviceCodeMappingTable.csv"; public static final String LOINC_IMAGING_DOCUMENT_CODES_FILE = "ImagingDocumentCodes.csv"; - private static final int LOG_INCREMENT = 100000; + public static final String LOINC_GROUP_FILE = "Group.csv"; + public static final String LOINC_GROUP_TERMS_FILE = "GroupLoincTerms.csv"; + public static final String LOINC_PARENT_GROUP_FILE = "ParentGroup.csv"; + private static final int LOG_INCREMENT = 1000; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TerminologyLoaderSvcImpl.class); + @Autowired private IHapiTerminologySvc myTermSvc; @Autowired(required = false) @@ -119,12 +124,20 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { } - private void iterateOverZipFile(LoadedFileDescriptors theDescriptors, String theFileNamePart, IRecordHandler theHandler, char theDelimiter, QuoteMode theQuoteMode) { + private void iterateOverZipFile(LoadedFileDescriptors theDescriptors, String theFileNamePart, IRecordHandler theHandler, char theDelimiter, QuoteMode theQuoteMode, boolean theIsPartialFilename) { + boolean foundMatch = false; for (FileDescriptor nextZipBytes : theDescriptors.getUncompressedFileDescriptors()) { String nextFilename = nextZipBytes.getFilename(); - if (nextFilename.contains(theFileNamePart)) { + boolean matches; + if (theIsPartialFilename) { + matches = nextFilename.contains(theFileNamePart); + } else { + matches = nextFilename.endsWith("/" + theFileNamePart) || nextFilename.equals(theFileNamePart); + } + if (matches) { ourLog.info("Processing file {}", nextFilename); + foundMatch = true; Reader reader; CSVParser parsed; @@ -149,6 +162,9 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { int nextLoggedCount = 0; while (iter.hasNext()) { CSVRecord nextRecord = iter.next(); + if (nextRecord.isConsistent()==false) { + continue; + } theHandler.accept(nextRecord); count++; if (count >= nextLoggedCount) { @@ -164,6 +180,10 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { } + if (!foundMatch) { + throw new InvalidRequestException("Did not find file matching " + theFileNamePart); + } + } @Override @@ -171,10 +191,7 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { LoadedFileDescriptors descriptors = new LoadedFileDescriptors(theFiles); List mandatoryFilenameFragments = Arrays.asList( LOINC_FILE, - LOINC_HIERARCHY_FILE); - descriptors.verifyMandatoryFilesExist(mandatoryFilenameFragments); - - List optionalFilenameFragments = Arrays.asList( + LOINC_HIERARCHY_FILE, LOINC_UPLOAD_PROPERTIES_FILE, LOINC_ANSWERLIST_FILE, LOINC_ANSWERLIST_LINK_FILE, @@ -189,6 +206,10 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_CSV, LOINC_IMAGING_DOCUMENT_CODES_FILE ); + descriptors.verifyMandatoryFilesExist(mandatoryFilenameFragments); + + List optionalFilenameFragments = Arrays.asList( + ); descriptors.verifyOptionalFilesExist(optionalFilenameFragments); ourLog.info("Beginning LOINC processing"); @@ -251,60 +272,75 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { // Part file handler = new LoincPartHandler(codeSystemVersion, code2concept); - iterateOverZipFile(theDescriptors, LOINC_PART_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_PART_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); Map partTypeAndPartNameToPartNumber = ((LoincPartHandler) handler).getPartTypeAndPartNameToPartNumber(); // Loinc Codes handler = new LoincHandler(codeSystemVersion, code2concept, propertyNamesToTypes, partTypeAndPartNameToPartNumber); - iterateOverZipFile(theDescriptors, LOINC_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // Loinc Hierarchy handler = new LoincHierarchyHandler(codeSystemVersion, code2concept); - iterateOverZipFile(theDescriptors, LOINC_HIERARCHY_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_HIERARCHY_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // Answer lists (ValueSets of potential answers/values for loinc "questions") handler = new LoincAnswerListHandler(codeSystemVersion, code2concept, valueSets, conceptMaps, uploadProperties); - iterateOverZipFile(theDescriptors, LOINC_ANSWERLIST_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_ANSWERLIST_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // Answer list links (connects loinc observation codes to answerlist codes) handler = new LoincAnswerListLinkHandler(code2concept, valueSets); - iterateOverZipFile(theDescriptors, LOINC_ANSWERLIST_LINK_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_ANSWERLIST_LINK_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); + + // RSNA Playbook file + // Note that this should come before the "Part Related Code Mapping" + // file because there are some duplicate mappings between these + // two files, and the RSNA Playbook file has more metadata + handler = new LoincRsnaPlaybookHandler(code2concept, valueSets, conceptMaps, uploadProperties); + iterateOverZipFile(theDescriptors, LOINC_RSNA_PLAYBOOK_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // Part link file handler = new LoincPartLinkHandler(codeSystemVersion, code2concept); - iterateOverZipFile(theDescriptors, LOINC_PART_LINK_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_PART_LINK_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // Part related code mapping - handler = new LoincPartRelatedCodeMappingHandler(codeSystemVersion, code2concept, valueSets, conceptMaps, uploadProperties); - iterateOverZipFile(theDescriptors, LOINC_PART_RELATED_CODE_MAPPING_FILE, handler, ',', QuoteMode.NON_NUMERIC); + handler = new LoincPartRelatedCodeMappingHandler(code2concept, valueSets, conceptMaps, uploadProperties); + iterateOverZipFile(theDescriptors, LOINC_PART_RELATED_CODE_MAPPING_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // Document Ontology File handler = new LoincDocumentOntologyHandler(code2concept, propertyNamesToTypes, valueSets, conceptMaps, uploadProperties); - iterateOverZipFile(theDescriptors, LOINC_DOCUMENT_ONTOLOGY_FILE, handler, ',', QuoteMode.NON_NUMERIC); - - // RSNA Playbook file - handler = new LoincRsnaPlaybookHandler(code2concept, valueSets, conceptMaps, uploadProperties); - iterateOverZipFile(theDescriptors, LOINC_RSNA_PLAYBOOK_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_DOCUMENT_ONTOLOGY_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // Top 2000 Codes - US handler = new LoincTop2000LabResultsUsHandler(code2concept, valueSets, conceptMaps, uploadProperties); - iterateOverZipFile(theDescriptors, LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // Top 2000 Codes - SI handler = new LoincTop2000LabResultsSiHandler(code2concept, valueSets, conceptMaps, uploadProperties); - iterateOverZipFile(theDescriptors, LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // Universal Lab Order ValueSet handler = new LoincUniversalOrderSetHandler(code2concept, valueSets, conceptMaps, uploadProperties); - iterateOverZipFile(theDescriptors, LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); // IEEE Medical Device Codes handler = new LoincIeeeMedicalDeviceCodeHandler(code2concept, valueSets, conceptMaps, uploadProperties); - iterateOverZipFile(theDescriptors, LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_CSV, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_CSV, handler, ',', QuoteMode.NON_NUMERIC, false); // Imaging Document Codes handler = new LoincImagingDocumentCodeHandler(code2concept, valueSets, conceptMaps, uploadProperties); - iterateOverZipFile(theDescriptors, LOINC_IMAGING_DOCUMENT_CODES_FILE, handler, ',', QuoteMode.NON_NUMERIC); + iterateOverZipFile(theDescriptors, LOINC_IMAGING_DOCUMENT_CODES_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); + + // Group File + handler = new LoincGroupFileHandler(code2concept, valueSets, conceptMaps, uploadProperties); + iterateOverZipFile(theDescriptors, LOINC_GROUP_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); + + // Group Terms File + handler = new LoincGroupTermsFileHandler(code2concept, valueSets, conceptMaps, uploadProperties); + iterateOverZipFile(theDescriptors, LOINC_GROUP_TERMS_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); + + // Parent Group File + handler = new LoincParentGroupFileHandler(code2concept, valueSets, conceptMaps, uploadProperties); + iterateOverZipFile(theDescriptors, LOINC_PARENT_GROUP_FILE, handler, ',', QuoteMode.NON_NUMERIC, false); IOUtils.closeQuietly(theDescriptors); @@ -332,18 +368,18 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { final Set validConceptIds = new HashSet<>(); IRecordHandler handler = new SctHandlerConcept(validConceptIds); - iterateOverZipFile(theDescriptors, SCT_FILE_CONCEPT, handler, '\t', null); + iterateOverZipFile(theDescriptors, SCT_FILE_CONCEPT, handler, '\t', null, true); ourLog.info("Have {} valid concept IDs", validConceptIds.size()); handler = new SctHandlerDescription(validConceptIds, code2concept, id2concept, codeSystemVersion); - iterateOverZipFile(theDescriptors, SCT_FILE_DESCRIPTION, handler, '\t', null); + iterateOverZipFile(theDescriptors, SCT_FILE_DESCRIPTION, handler, '\t', null, true); ourLog.info("Got {} concepts, cloning map", code2concept.size()); final HashMap rootConcepts = new HashMap<>(code2concept); handler = new SctHandlerRelationship(codeSystemVersion, rootConcepts, code2concept); - iterateOverZipFile(theDescriptors, SCT_FILE_RELATIONSHIP, handler, '\t', null); + iterateOverZipFile(theDescriptors, SCT_FILE_RELATIONSHIP, handler, '\t', null, true); IOUtils.closeQuietly(theDescriptors); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/BaseLoincHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/BaseLoincHandler.java index 49ae11ce88f..35a405031ca 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/BaseLoincHandler.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/BaseLoincHandler.java @@ -26,6 +26,8 @@ import org.hl7.fhir.r4.model.ConceptMap; import org.hl7.fhir.r4.model.ContactPoint; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.ValueSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.List; @@ -35,8 +37,9 @@ import java.util.Properties; import static org.apache.commons.lang3.StringUtils.*; public abstract class BaseLoincHandler implements IRecordHandler { - + private static final Logger ourLog = LoggerFactory.getLogger(BaseLoincHandler.class); public static final String LOINC_COPYRIGHT_STATEMENT = "This content from LOINC® is copyright © 1995 Regenstrief Institute, Inc. and the LOINC Committee, and available at no cost under the license at https://loinc.org/license/"; + /** * This is NOT the LOINC CodeSystem URI! It is just * the website URL to LOINC. @@ -52,8 +55,10 @@ public abstract class BaseLoincHandler implements IRecordHandler { BaseLoincHandler(Map theCode2Concept, List theValueSets, List theConceptMaps, Properties theUploadProperties) { myValueSets = theValueSets; + myValueSets.forEach(t -> myIdToValueSet.put(t.getId(), t)); myCode2Concept = theCode2Concept; myConceptMaps = theConceptMaps; + myConceptMaps.forEach(t -> myIdToConceptMaps.put(t.getId(), t)); myUploadProperties = theUploadProperties; } @@ -80,10 +85,9 @@ public abstract class BaseLoincHandler implements IRecordHandler { String displayName = theDisplayName; if (isBlank(displayName)) { - for (TermConcept next : myCode2Concept.values()) { - if (next.getCode().equals(theCode)) { - displayName = next.getDisplay(); - } + TermConcept concept = myCode2Concept.get(theCode); + if (concept != null) { + displayName = concept.getDisplay(); } } @@ -176,6 +180,8 @@ public abstract class BaseLoincHandler implements IRecordHandler { .setCode(theMapping.getTargetCode()) .setDisplay(theMapping.getTargetDisplay()) .setEquivalence(theMapping.getEquivalence()); + } else { + ourLog.info("Not going to add a mapping from [{}/{}] to [{}/{}] because one already exists", theMapping.getSourceCodeSystem(), theMapping.getSourceCode(), theMapping.getTargetCodeSystem(), theMapping.getTargetCode()); } } @@ -192,7 +198,6 @@ public abstract class BaseLoincHandler implements IRecordHandler { vs.setUrl(theValueSetUri); vs.setId(theValueSetId); vs.setVersion(version); - vs.setName(theValueSetName); vs.setStatus(Enumerations.PublicationStatus.ACTIVE); vs.setPublisher(REGENSTRIEF_INSTITUTE_INC); vs.addContact() @@ -206,6 +211,11 @@ public abstract class BaseLoincHandler implements IRecordHandler { } else { vs = myIdToValueSet.get(theValueSetId); } + + if (isBlank(vs.getName()) && isNotBlank(theValueSetName)) { + vs.setName(theValueSetName); + } + return vs; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincGroupFileHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincGroupFileHandler.java new file mode 100644 index 00000000000..775bdf59348 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincGroupFileHandler.java @@ -0,0 +1,62 @@ +package ca.uhn.fhir.jpa.term.loinc; + +/*- + * #%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.entity.TermConcept; +import ca.uhn.fhir.jpa.term.IRecordHandler; +import org.apache.commons.csv.CSVRecord; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.ValueSet; + +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.apache.commons.lang3.StringUtils.trim; + +public class LoincGroupFileHandler extends BaseLoincHandler implements IRecordHandler { + + public static final String VS_URI_PREFIX = "http://loinc.org/vs/"; + + public LoincGroupFileHandler(Map theCode2concept, List theValueSets, List theConceptMaps, Properties theUploadProperties) { + super(theCode2concept, theValueSets, theConceptMaps, theUploadProperties); + } + + @Override + public void accept(CSVRecord theRecord) { + //"ParentGroupId","GroupId","Group","Archetype","Status","VersionFirstReleased" + String parentGroupId = trim(theRecord.get("ParentGroupId")); + String groupId = trim(theRecord.get("GroupId")); + String groupName = trim(theRecord.get("Group")); + + ValueSet parentValueSet = getValueSet(parentGroupId, VS_URI_PREFIX + parentGroupId, null, null); + parentValueSet + .getCompose() + .getIncludeFirstRep() + .addValueSet(VS_URI_PREFIX + groupId); + + // Create group to set its name (terms are added in a different + // handler) + getValueSet(groupId, VS_URI_PREFIX + groupId, groupName, null); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincGroupTermsFileHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincGroupTermsFileHandler.java new file mode 100644 index 00000000000..d4d9512dd4d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincGroupTermsFileHandler.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.jpa.term.loinc; + +/*- + * #%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.entity.TermConcept; +import ca.uhn.fhir.jpa.term.IHapiTerminologyLoaderSvc; +import ca.uhn.fhir.jpa.term.IRecordHandler; +import org.apache.commons.csv.CSVRecord; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.ValueSet; + +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.apache.commons.lang3.StringUtils.trim; + +public class LoincGroupTermsFileHandler extends BaseLoincHandler implements IRecordHandler { + + public LoincGroupTermsFileHandler(Map theCode2concept, List theValueSets, List theConceptMaps, Properties theUploadProperties) { + super(theCode2concept, theValueSets, theConceptMaps, theUploadProperties); + } + + @Override + public void accept(CSVRecord theRecord) { + //"Category","GroupId","Archetype","LoincNumber","LongCommonName" + String groupId = trim(theRecord.get("GroupId")); + String loincNumber = trim(theRecord.get("LoincNumber")); + + ValueSet valueSet = getValueSet(groupId, LoincGroupFileHandler.VS_URI_PREFIX + groupId, null, null); + addCodeAsIncludeToValueSet(valueSet, IHapiTerminologyLoaderSvc.LOINC_URI, loincNumber, null); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincHandler.java index 97fd4da07b6..aef6a45e4bc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincHandler.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincHandler.java @@ -39,6 +39,7 @@ import static org.apache.commons.lang3.StringUtils.trim; public class LoincHandler implements IRecordHandler { + private static final Logger ourLog = LoggerFactory.getLogger(LoincHandler.class); private final Map myCode2Concept; private final TermCodeSystemVersion myCodeSystemVersion; private final Map myPropertyNames; @@ -86,7 +87,17 @@ public class LoincHandler implements IRecordHandler { concept.addPropertyString(nextPropertyName, nextPropertyValue); break; case CODING: - PartTypeAndPartName key = new PartTypeAndPartName(nextPropertyName, nextPropertyValue); + // FIXME: handle "Ser/Plas^Donor" + String propertyValue = nextPropertyValue; + if (nextPropertyName.equals("COMPONENT")) { + if (propertyValue.contains("^")) { + propertyValue = propertyValue.substring(0, propertyValue.indexOf("^")); + } else if (propertyValue.contains("/")) { + propertyValue = propertyValue.substring(0, propertyValue.indexOf("/")); + } + } + + PartTypeAndPartName key = new PartTypeAndPartName(nextPropertyName, propertyValue); String partNumber = myPartTypeAndPartNameToPartNumber.get(key); if (partNumber == null && nextPropertyName.equals("TIME_ASPCT")) { @@ -106,11 +117,12 @@ public class LoincHandler implements IRecordHandler { continue; } -// Validate.notBlank(partNumber, "Unknown part: " + key); if (isNotBlank(partNumber)) { concept.addPropertyCoding(nextPropertyName, IHapiTerminologyLoaderSvc.LOINC_URI, partNumber, nextPropertyValue); } else { - ourLog.warn("Unable to find part code with TYPE[{}] and NAME[{}]", key.getPartType(), key.getPartName()); + String msg = "Unable to find part code with TYPE[" + key.getPartType() + "] and NAME[" + nextPropertyValue + "] (using name " + propertyValue + ")"; + ourLog.warn(msg); +// throw new InternalErrorException(msg); } break; case DECIMAL: @@ -129,5 +141,4 @@ public class LoincHandler implements IRecordHandler { myCode2Concept.put(code, concept); } } -private static final Logger ourLog = LoggerFactory.getLogger(LoincHandler.class); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincIeeeMedicalDeviceCodeHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincIeeeMedicalDeviceCodeHandler.java index 204497bc5ce..dad747ba772 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincIeeeMedicalDeviceCodeHandler.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincIeeeMedicalDeviceCodeHandler.java @@ -37,7 +37,7 @@ import static org.apache.commons.lang3.StringUtils.trim; public class LoincIeeeMedicalDeviceCodeHandler extends BaseLoincHandler implements IRecordHandler { public static final String LOINC_IEEE_CM_ID = "LOINC-IEEE-MEDICAL-DEVICE-CM"; - public static final String LOINC_IEEE_CM_URI = "http://loinc.org/fhir/loinc-ieee-device-code-mappings"; + public static final String LOINC_IEEE_CM_URI = "http://loinc.org/cm/loinc-to-ieee-device-codes"; public static final String LOINC_IEEE_CM_NAME = "LOINC/IEEE Device Code Mappings"; private static final String CM_COPYRIGHT = "This content from LOINC® is copyright © 1995 Regenstrief Institute, Inc. and the LOINC Committee, and available at no cost under the license at https://loinc.org/license/. The LOINC/IEEE Medical Device Code Mapping Table contains content from IEEE (http://ieee.org), copyright © 2017 IEEE."; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincImagingDocumentCodeHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincImagingDocumentCodeHandler.java index e61f33f402d..13de5d3862b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincImagingDocumentCodeHandler.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincImagingDocumentCodeHandler.java @@ -36,7 +36,7 @@ import static org.apache.commons.lang3.StringUtils.trim; public class LoincImagingDocumentCodeHandler extends BaseLoincHandler implements IRecordHandler { public static final String VS_ID = "loinc-imaging-document-codes"; - public static final String VS_URI = "http://loinc.org/fhir/loinc-imaging-document-codes"; + public static final String VS_URI = "http://loinc.org/vs/loinc-imaging-document-codes"; public static final String VS_NAME = "LOINC Imaging Document Codes"; public LoincImagingDocumentCodeHandler(Map theCode2concept, List theValueSets, List theConceptMaps, Properties theUploadProperties) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincParentGroupFileHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincParentGroupFileHandler.java new file mode 100644 index 00000000000..37d0fe5fa06 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincParentGroupFileHandler.java @@ -0,0 +1,51 @@ +package ca.uhn.fhir.jpa.term.loinc; + +/*- + * #%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.entity.TermConcept; +import ca.uhn.fhir.jpa.term.IRecordHandler; +import org.apache.commons.csv.CSVRecord; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.ValueSet; + +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.apache.commons.lang3.StringUtils.trim; + +public class LoincParentGroupFileHandler extends BaseLoincHandler implements IRecordHandler { + + public LoincParentGroupFileHandler(Map theCode2concept, List theValueSets, List theConceptMaps, Properties theUploadProperties) { + super(theCode2concept, theValueSets, theConceptMaps, theUploadProperties); + } + + @Override + public void accept(CSVRecord theRecord) { + // "ParentGroupId","ParentGroup","Status" + String parentGroupId = trim(theRecord.get("ParentGroupId")); + String parentGroupName = trim(theRecord.get("ParentGroup")); + + getValueSet(parentGroupId, LoincGroupFileHandler.VS_URI_PREFIX + parentGroupId, parentGroupName, null); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincPartRelatedCodeMappingHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincPartRelatedCodeMappingHandler.java index 1c71f168da7..daa122c4164 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincPartRelatedCodeMappingHandler.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincPartRelatedCodeMappingHandler.java @@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.term.loinc; * #L% */ -import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.term.IHapiTerminologyLoaderSvc; import ca.uhn.fhir.jpa.term.IRecordHandler; @@ -41,23 +40,23 @@ public class LoincPartRelatedCodeMappingHandler extends BaseLoincHandler impleme public static final String LOINC_SCT_PART_MAP_ID = "loinc-parts-to-snomed-ct"; public static final String LOINC_SCT_PART_MAP_URI = "http://loinc.org/cm/loinc-parts-to-snomed-ct"; - public static final String LOINC_SCT_PART_MAP_NAME = "LOINC Part Map to SNOMED CT"; - public static final String LOINC_RXNORM_PART_MAP_ID = "loinc-parts-to-rxnorm"; - public static final String LOINC_RXNORM_PART_MAP_URI = "http://loinc.org/cm/loinc-parts-to-rxnorm"; - public static final String LOINC_RXNORM_PART_MAP_NAME = "LOINC Part Map to RxNORM"; - public static final String LOINC_RADLEX_PART_MAP_ID = "loinc-parts-to-radlex"; - public static final String LOINC_RADLEX_PART_MAP_URI = "http://loinc.org/cm/loinc-parts-to-radlex"; - public static final String LOINC_RADLEX_PART_MAP_NAME = "LOINC Part Map to RADLEX"; + public static final String LOINC_TERM_TO_RPID_PART_MAP_ID = "loinc-term-to-rpids"; + public static final String LOINC_TERM_TO_RPID_PART_MAP_URI = "http://loinc.org/cm/loinc-to-rpids"; + public static final String LOINC_TERM_TO_RPID_PART_MAP_NAME = "LOINC Terms to RadLex RPIDs"; + public static final String LOINC_PART_TO_RID_PART_MAP_ID = "loinc-part-to-rids"; + public static final String LOINC_PART_TO_RID_PART_MAP_URI = "http://loinc.org/cm/loinc-to-rids"; + public static final String LOINC_PART_TO_RID_PART_MAP_NAME = "LOINC Parts to RadLex RIDs"; + private static final String LOINC_SCT_PART_MAP_NAME = "LOINC Part Map to SNOMED CT"; + private static final String LOINC_RXNORM_PART_MAP_ID = "loinc-parts-to-rxnorm"; + private static final String LOINC_RXNORM_PART_MAP_URI = "http://loinc.org/cm/loinc-parts-to-rxnorm"; + private static final String LOINC_RXNORM_PART_MAP_NAME = "LOINC Part Map to RxNORM"; private static final String CM_COPYRIGHT = "This content from LOINC® is copyright © 1995 Regenstrief Institute, Inc. and the LOINC Committee, and available at no cost under the license at https://loinc.org/license/. The LOINC Part File, LOINC/SNOMED CT Expression Association and Map Sets File, RELMA database and associated search index files include SNOMED Clinical Terms (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights are reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO. Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. Under the terms of the Affiliate License, use of SNOMED CT in countries that are not IHTSDO Members is subject to reporting and fee payment obligations. However, IHTSDO agrees to waive the requirements to report and pay fees for use of SNOMED CT content included in the LOINC Part Mapping and LOINC Term Associations for purposes that support or enable more effective use of LOINC. This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov."; - private final Map myCode2Concept; - private final TermCodeSystemVersion myCodeSystemVersion; - private final List myConceptMaps; + private static final String LOINC_PUBCHEM_PART_MAP_URI = "http://loinc.org/cm/loinc-parts-to-pubchem"; + private static final String LOINC_PUBCHEM_PART_MAP_ID = "loinc-parts-to-pubchem"; + private static final String LOINC_PUBCHEM_PART_MAP_NAME = "LOINC Part Map to PubChem"; - public LoincPartRelatedCodeMappingHandler(TermCodeSystemVersion theCodeSystemVersion, Map theCode2concept, List theValueSets, List theConceptMaps, Properties theUploadProperties) { + public LoincPartRelatedCodeMappingHandler(Map theCode2concept, List theValueSets, List theConceptMaps, Properties theUploadProperties) { super(theCode2concept, theValueSets, theConceptMaps, theUploadProperties); - myCodeSystemVersion = theCodeSystemVersion; - myCode2Concept = theCode2concept; - myConceptMaps = theConceptMaps; } @Override @@ -108,12 +107,20 @@ public class LoincPartRelatedCodeMappingHandler extends BaseLoincHandler impleme loincPartMapName = LOINC_RXNORM_PART_MAP_NAME; break; case "http://www.radlex.org": - loincPartMapId = LOINC_RADLEX_PART_MAP_ID; - loincPartMapUri = LOINC_RADLEX_PART_MAP_URI; - loincPartMapName = LOINC_RADLEX_PART_MAP_NAME; + loincPartMapId = LOINC_PART_TO_RID_PART_MAP_ID; + loincPartMapUri = LOINC_PART_TO_RID_PART_MAP_URI; + loincPartMapName = LOINC_PART_TO_RID_PART_MAP_NAME; + break; + case "http://pubchem.ncbi.nlm.nih.gov": + loincPartMapId = LOINC_PUBCHEM_PART_MAP_ID; + loincPartMapUri = LOINC_PUBCHEM_PART_MAP_URI; + loincPartMapName = LOINC_PUBCHEM_PART_MAP_NAME; break; default: - throw new InternalErrorException("Don't know how to handle mapping to system: " + extCodeSystem); + loincPartMapId = extCodeSystem.replaceAll("[^a-zA-Z]", ""); + loincPartMapUri = extCodeSystem; + loincPartMapName = "Unknown Mapping"; + break; } addConceptMapEntry( diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincRsnaPlaybookHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincRsnaPlaybookHandler.java index 2bc0e4e8015..5e404a2f6aa 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincRsnaPlaybookHandler.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincRsnaPlaybookHandler.java @@ -39,21 +39,16 @@ public class LoincRsnaPlaybookHandler extends BaseLoincHandler implements IRecor public static final String RSNA_CODES_VS_ID = "loinc-rsna-radiology-playbook"; public static final String RSNA_CODES_VS_URI = "http://loinc.org/vs/loinc-rsna-radiology-playbook"; public static final String RSNA_CODES_VS_NAME = "LOINC/RSNA Radiology Playbook"; - public static final String RID_MAPPING_CM_ID = "LOINC-TO-RID-CODES-CM"; - public static final String RID_MAPPING_CM_URI = "http://loinc.org/rid-codes"; - public static final String RID_MAPPING_CM_NAME = "RSNA Playbook RID Codes Mapping"; public static final String RID_CS_URI = "http://www.radlex.org"; - public static final String RPID_MAPPING_CM_ID = "LOINC-TO-RPID-CODES-CM"; - public static final String RPID_MAPPING_CM_URI = "http://loinc.org/rpid-codes"; - public static final String RPID_MAPPING_CM_NAME = "RSNA Playbook RPID Codes Mapping"; /* - * About these being the same - Per Dan: + * About these being the same - Per Dan Vreeman: * We had some discussion about this, and both - * RIDs (RadLex clinical terms) and RPIDs (Radlex Playbook Ids) + * RIDs (RadLex clinical terms) and RPIDs (Radlex Playbook Ids) * belong to the same "code system" since they will never collide. * The codesystem uri is "http://www.radlex.org". FYI, that's * now listed on the FHIR page: * https://www.hl7.org/fhir/terminologies-systems.html + * -ja */ public static final String RPID_CS_URI = RID_CS_URI; private static final String CM_COPYRIGHT = "This content from LOINC® is copyright © 1995 Regenstrief Institute, Inc. and the LOINC Committee, and available at no cost under the license at https://loinc.org/license/. The LOINC/RSNA Radiology Playbook and the LOINC Part File contain content from RadLex® (http://rsna.org/RadLex.aspx), copyright © 2005-2017, The Radiological Society of North America, Inc., available at no cost under the license at http://www.rsna.org/uploadedFiles/RSNA/Content/Informatics/RadLex_License_Agreement_and_Terms_of_Use_V2_Final.pdf."; @@ -179,9 +174,9 @@ public class LoincRsnaPlaybookHandler extends BaseLoincHandler implements IRecor if (isNotBlank(rid)) { addConceptMapEntry( new ConceptMapping() - .setConceptMapId(RID_MAPPING_CM_ID) - .setConceptMapUri(RID_MAPPING_CM_URI) - .setConceptMapName(RID_MAPPING_CM_NAME) + .setConceptMapId(LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_ID) + .setConceptMapUri(LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_URI) + .setConceptMapName(LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_NAME) .setSourceCodeSystem(IHapiTerminologyLoaderSvc.LOINC_URI) .setSourceCode(partNumber) .setSourceDisplay(partName) @@ -196,9 +191,9 @@ public class LoincRsnaPlaybookHandler extends BaseLoincHandler implements IRecor if (isNotBlank(rpid)) { addConceptMapEntry( new ConceptMapping() - .setConceptMapId(RPID_MAPPING_CM_ID) - .setConceptMapUri(RPID_MAPPING_CM_URI) - .setConceptMapName(RPID_MAPPING_CM_NAME) + .setConceptMapId(LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_ID) + .setConceptMapUri(LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_URI) + .setConceptMapName(LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_NAME) .setSourceCodeSystem(IHapiTerminologyLoaderSvc.LOINC_URI) .setSourceCode(loincNumber) .setSourceDisplay(longCommonName) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincUniversalOrderSetHandler.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincUniversalOrderSetHandler.java index c8d0ffb7179..099cd8cb7e4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincUniversalOrderSetHandler.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/loinc/LoincUniversalOrderSetHandler.java @@ -34,7 +34,7 @@ import static org.apache.commons.lang3.StringUtils.trim; public class LoincUniversalOrderSetHandler extends BaseLoincHandler implements IRecordHandler { public static final String VS_ID = "loinc-universal-order-set-vs"; - public static final String VS_URI = "http://loinc.org/fhir/loinc-universal-order-set"; + public static final String VS_URI = "http://loinc.org/vs/loinc-universal-order-set"; public static final String VS_NAME = "LOINC Universal Order Set"; public LoincUniversalOrderSetHandler(Map theCode2concept, List theValueSets, List theConceptMaps, Properties theUploadProperties) { 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 e5567524b05..812d63250ad 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 @@ -150,6 +150,11 @@ public class JpaConstants { */ public static final String OPERATION_EVERYTHING = "$everything"; + /** + * Operation name for the $process-message operation + */ + public static final String OPERATION_PROCESS_MESSAGE = "$process-message"; + /** * Operation name for the $meta-delete operation */ 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 b46d0137e75..f1cc3ec861e 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 @@ -83,14 +83,19 @@ public class ReindexController implements IReindexController { 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 { + ourLog.info("Reindex pass complete, {} remaining resource to index", count); myDontReindexUntil = null; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainR4.java index 1385e89dde6..6df063b1455 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainR4.java @@ -24,7 +24,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; -import org.hl7.fhir.r4.hapi.ctx.ValidationSupportChain; +import org.hl7.fhir.r4.hapi.validation.ValidationSupportChain; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; 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 3de0c6202fe..21edfb9c3d5 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 @@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -43,11 +44,6 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { private Exception myLastStackTrace; private String myLastStackTraceThreadName; - @Bean(name="maxDatabaseThreadsForTest") - public Integer getMaxThread(){ - return ourMaxThreads; - } - @Bean() public DaoConfig daoConfig() { return new DaoConfig(); @@ -131,6 +127,11 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { return retVal; } + @Bean(name = "maxDatabaseThreadsForTest") + public Integer getMaxThread() { + return ourMaxThreads; + } + private Properties jpaProperties() { Properties extraProperties = new Properties(); extraProperties.put("hibernate.format_sql", "true"); @@ -165,4 +166,9 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { 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 a7d9cccfe12..eb692759bcd 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 @@ -6,14 +6,9 @@ import ca.uhn.fhir.jpa.subscription.email.IEmailSender; import ca.uhn.fhir.jpa.subscription.email.JavaMailEmailSender; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.ResultSeverityEnum; -import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.apache.commons.dbcp2.BasicDataSource; import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -22,13 +17,11 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.env.Environment; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; import java.sql.Connection; -import java.sql.SQLException; import java.util.Properties; import java.util.concurrent.TimeUnit; @@ -194,23 +187,4 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { } - public class UnregisterScheduledProcessor implements BeanFactoryPostProcessor { - - private final Environment myEnvironment; - - public UnregisterScheduledProcessor(Environment theEnv) { - myEnvironment = theEnv; - } - - @Override - public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException { - String schedulingDisabled = myEnvironment.getProperty("scheduling_disabled"); - if ("true".equals(schedulingDisabled)) { - for (String beanName : beanFactory.getBeanNamesForType(ScheduledAnnotationBeanPostProcessor.class)) { - ((DefaultListableBeanFactory) beanFactory).removeBeanDefinition(beanName); - } - } - } - } - } 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 0a8df9f8bc5..5557e57e067 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 @@ -7,15 +7,13 @@ 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.hibernate.jpa.HibernatePersistenceProvider; import org.hibernate.query.criteria.LiteralHandlingMode; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; -import org.springframework.orm.hibernate5.HibernateExceptionTranslator; +import org.springframework.core.env.Environment; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.HibernateJpaDialect; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.persistence.EntityManagerFactory; @@ -107,7 +105,7 @@ public class TestR4Config extends BaseJavaConfigR4 { DataSource dataSource = ProxyDataSourceBuilder .create(retVal) -// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") + .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") .logSlowQueryBySlf4j(10, TimeUnit.SECONDS) .countQuery(new ThreadQueryCountHolder()) .build(); @@ -163,6 +161,11 @@ public class TestR4Config extends BaseJavaConfigR4 { 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/UnregisterScheduledProcessor.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/UnregisterScheduledProcessor.java new file mode 100644 index 00000000000..cf5943b37d3 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/UnregisterScheduledProcessor.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.jpa.config; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor; +import org.springframework.scheduling.concurrent.ExecutorConfigurationSupport; + +/** + * This bean postprocessor disables all scheduled tasks. It is intended + * only to be used in unit tests in circumstances where scheduled + * tasks cause issues. + */ +public class UnregisterScheduledProcessor implements BeanFactoryPostProcessor { + + private final Environment myEnvironment; + + public UnregisterScheduledProcessor(Environment theEnv) { + myEnvironment = theEnv; + } + + @Override + public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException { + String schedulingDisabled = myEnvironment.getProperty("scheduling_disabled"); + if ("true".equals(schedulingDisabled)) { + for (String beanName : beanFactory.getBeanNamesForType(ScheduledAnnotationBeanPostProcessor.class)) { + ((DefaultListableBeanFactory) beanFactory).removeBeanDefinition(beanName); + } + + for (String beanName : beanFactory.getBeanNamesForType(ExecutorConfigurationSupport.class)) { + ExecutorConfigurationSupport executorConfigSupport = ((DefaultListableBeanFactory) beanFactory).getBean(beanName, ExecutorConfigurationSupport.class); + executorConfigSupport.shutdown(); + } + } + + } +} 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 0528c8d806e..556ff29c3ed 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 @@ -331,7 +331,6 @@ public abstract class BaseJpaTest { theSystemDao.expunge(new ExpungeOptions().setExpungeEverything(true)); theDaoConfig.setExpungeEnabled(expungeEnabled); - theSearchParamPresenceSvc.flushCachesForUnitTest(); theSearchParamRegistry.forceRefresh(); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ConceptMapTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ConceptMapTest.java index 62a41cde417..62311c3a6d0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ConceptMapTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ConceptMapTest.java @@ -24,17 +24,33 @@ public class FhirResourceDaoDstu3ConceptMapTest extends BaseJpaDstu3Test { private IIdType myConceptMapId; - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - @Before @Transactional public void before02() { myConceptMapId = myConceptMapDao.create(createConceptMap(), mySrd).getId().toUnqualifiedVersionless(); } + @Test + public void testDeleteConceptMap() { + myConceptMapDao.delete(myConceptMapId); + + new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus theStatus) { + TranslationRequest translationRequest = new TranslationRequest(); + translationRequest.getCodeableConcept().addCoding() + .setSystem(CS_URL) + .setCode("12345"); + translationRequest.setTargetSystem(new UriType(CS_URL_3)); + + TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); + + assertFalse(translationResult.getResult().booleanValue()); + } + }); + + } + @Test public void testTranslateByCodeSystemsAndSourceCodeOneToMany() { ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); @@ -81,4 +97,9 @@ public class FhirResourceDaoDstu3ConceptMapTest extends BaseJpaDstu3Test { } }); } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } } 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 f25a81db2ee..8f4649dd853 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 @@ -64,7 +64,6 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu } } - @Test public void testCreateInvalidParamNoPath() { SearchParameter fooSp = new SearchParameter(); @@ -858,6 +857,49 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu } + @Test + public void testSearchParameterDescendsIntoContainedResource() { + SearchParameter sp = new SearchParameter(); + sp.addBase("Observation"); + sp.setCode("specimencollectedtime"); + sp.setType(Enumerations.SearchParamType.DATE); + sp.setTitle("Observation Specimen Collected Time"); + sp.setExpression("Observation.specimen.resolve().receivedTime"); + sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); + mySearchParameterDao.create(sp); + + mySearchParamRegsitry.forceRefresh(); + + Specimen specimen = new Specimen(); + specimen.setId("#FOO"); + specimen.setReceivedTimeElement(new DateTimeType("2011-01-01")); + Observation o = new Observation(); + o.setId("O1"); + o.getContained().add(specimen); + o.setStatus(Observation.ObservationStatus.FINAL); + o.setSpecimen(new Reference("#FOO")); + myObservationDao.update(o); + + specimen = new Specimen(); + specimen.setId("#FOO"); + specimen.setReceivedTimeElement(new DateTimeType("2011-01-03")); + o = new Observation(); + o.setId("O2"); + o.getContained().add(specimen); + o.setStatus(Observation.ObservationStatus.FINAL); + o.setSpecimen(new Reference("#FOO")); + myObservationDao.update(o); + + SearchParameterMap params = new SearchParameterMap(); + params.add("specimencollectedtime", new DateParam("2011-01-01")); + IBundleProvider outcome = myObservationDao.search(params); + List ids = toUnqualifiedVersionlessIdValues(outcome); + ourLog.info("IDS: " + ids); + assertThat(ids, contains("Observation/O1")); + } + @Test public void testSearchWithCustomParam() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java index 8ebb3497ea4..068acbd9de4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/SearchParamExtractorDstu3Test.java @@ -7,6 +7,7 @@ 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 org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; @@ -81,7 +82,8 @@ public class SearchParamExtractorDstu3Test { } }; - SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(ourCtx, ourValidationSupport, searchParamRegistry); + SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new DaoConfig(), ourCtx, ourValidationSupport, searchParamRegistry); + extractor.start(); Set tokens = extractor.extractSearchParamTokens(new ResourceTable(), obs); assertEquals(1, tokens.size()); ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.iterator().next(); 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 5c48b74c639..0375a20d664 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 @@ -66,8 +66,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Autowired protected IResourceLinkDao myResourceLinkDao; @Autowired - protected ISearchParamDao mySearchParamDao; - @Autowired protected ISearchParamPresentDao mySearchParamPresentDao; @Autowired protected IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao; @@ -106,6 +104,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Qualifier("myConceptMapDaoR4") protected IFhirResourceDaoConceptMap myConceptMapDao; @Autowired + protected ITermConceptDao myTermConceptDao; + @Autowired @Qualifier("myConditionDaoR4") protected IFhirResourceDao myConditionDao; @Autowired @@ -170,6 +170,12 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Qualifier("myPatientDaoR4") protected IFhirResourceDaoPatient myPatientDao; @Autowired + protected IResourceTableDao myResourceTableDao; + @Autowired + protected IResourceHistoryTableDao myResourceHistoryTableDao; + @Autowired + protected IForcedIdDao myForcedIdDao; + @Autowired @Qualifier("myCoverageDaoR4") protected IFhirResourceDao myCoverageDao; @Autowired @@ -188,10 +194,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Qualifier("myResourceProvidersR4") protected Object myResourceProviders; @Autowired - protected IResourceTableDao myResourceTableDao; - @Autowired - protected IResourceHistoryTableDao myResourceHistoryTableDao; - @Autowired protected IResourceTagDao myResourceTagDao; @Autowired protected ISearchCoordinatorSvc mySearchCoordinatorSvc; @@ -257,6 +259,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { myDaoConfig.setExpireSearchResultsAfterMillis(new DaoConfig().getExpireSearchResultsAfterMillis()); myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis()); myDaoConfig.setSuppressUpdatesWithNoChange(new DaoConfig().isSuppressUpdatesWithNoChange()); + myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); } @After @@ -283,7 +286,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Before public void beforeFlushFT() { - runInTransaction(()->{ + runInTransaction(() -> { FullTextEntityManager ftem = Search.getFullTextEntityManager(myEntityManager); ftem.purgeAll(ResourceTable.class); ftem.purgeAll(ResourceIndexedSearchParamString.class); @@ -314,6 +317,11 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { return myFhirCtx; } + @Override + protected PlatformTransactionManager getTxManager() { + return myTxManager; + } + protected T loadResourceFromClasspath(Class type, String resourceName) throws IOException { InputStream stream = FhirResourceDaoDstu2SearchNoFtTest.class.getResourceAsStream(resourceName); if (stream == null) { @@ -324,11 +332,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { return newJsonParser.parseResource(type, string); } - @Override - protected PlatformTransactionManager getTxManager() { - return myTxManager; - } - @AfterClass public static void afterClassClearContextBaseJpaR4Test() throws Exception { ourValueSetDao.purgeCaches(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConceptMapTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConceptMapTest.java index 3a0cd357688..c96b8e5ce18 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConceptMapTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ConceptMapTest.java @@ -26,17 +26,33 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test { private IIdType myConceptMapId; - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - @Before @Transactional public void before02() { myConceptMapId = myConceptMapDao.create(createConceptMap(), mySrd).getId().toUnqualifiedVersionless(); } + @Test + public void testDeleteConceptMap() { + myConceptMapDao.delete(myConceptMapId); + + new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus theStatus) { + TranslationRequest translationRequest = new TranslationRequest(); + translationRequest.getCodeableConcept().addCoding() + .setSystem(CS_URL) + .setCode("12345"); + translationRequest.setTargetSystem(new UriType(CS_URL_3)); + + TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); + + assertFalse(translationResult.getResult().booleanValue()); + } + }); + + } + @Test public void testTranslateByCodeSystemsAndSourceCodeOneToMany() { ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); @@ -205,6 +221,98 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test { }); } + @Test + public void testTranslateUsingPredicatesWithSourceAndTargetSystem2() { + ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); + + ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); + + new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus theStatus) { + /* + * Provided: + * source code + * source code system + * target code system #2 + */ + TranslationRequest translationRequest = new TranslationRequest(); + translationRequest.getCodeableConcept().addCoding() + .setSystem(CS_URL) + .setCode("12345"); + translationRequest.setTargetSystem(new UriType(CS_URL_2)); + + TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); + + assertTrue(translationResult.getResult().booleanValue()); + assertEquals("Matches found!", translationResult.getMessage().getValueAsString()); + + assertEquals(1, translationResult.getMatches().size()); + + TranslationMatch translationMatch = translationResult.getMatches().get(0); + assertEquals(ConceptMapEquivalence.EQUAL.toCode(), translationMatch.getEquivalence().getCode()); + Coding concept = translationMatch.getConcept(); + assertEquals("34567", concept.getCode()); + assertEquals("Target Code 34567", concept.getDisplay()); + assertEquals(CS_URL_2, concept.getSystem()); + assertEquals("Version 2", concept.getVersion()); + assertFalse(concept.getUserSelected()); + assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); + } + }); + } + + @Test + public void testTranslateUsingPredicatesWithSourceAndTargetSystem3() { + ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); + + ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); + + new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus theStatus) { + /* + * Provided: + * source code + * source code system + * target code system #3 + */ + TranslationRequest translationRequest = new TranslationRequest(); + translationRequest.getCodeableConcept().addCoding() + .setSystem(CS_URL) + .setCode("12345"); + translationRequest.setTargetSystem(new UriType(CS_URL_3)); + + TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); + + assertTrue(translationResult.getResult().booleanValue()); + assertEquals("Matches found!", translationResult.getMessage().getValueAsString()); + + assertEquals(2, translationResult.getMatches().size()); + + TranslationMatch translationMatch = translationResult.getMatches().get(0); + assertEquals(ConceptMapEquivalence.EQUAL.toCode(), translationMatch.getEquivalence().getCode()); + Coding concept = translationMatch.getConcept(); + assertEquals("56789", concept.getCode()); + assertEquals("Target Code 56789", concept.getDisplay()); + assertEquals(CS_URL_3, concept.getSystem()); + assertEquals("Version 4", concept.getVersion()); + assertFalse(concept.getUserSelected()); + assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); + + translationMatch = translationResult.getMatches().get(1); + assertEquals(ConceptMapEquivalence.WIDER.toCode(), translationMatch.getEquivalence().getCode()); + concept = translationMatch.getConcept(); + assertEquals("67890", concept.getCode()); + assertEquals("Target Code 67890", concept.getDisplay()); + assertEquals(CS_URL_3, concept.getSystem()); + assertEquals("Version 4", concept.getVersion()); + assertFalse(concept.getUserSelected()); + assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); + } + }); + } + @Test public void testTranslateUsingPredicatesWithSourceSystem() { ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); @@ -356,98 +464,6 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test { }); } - @Test - public void testTranslateUsingPredicatesWithSourceAndTargetSystem2() { - ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); - - ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); - - new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus theStatus) { - /* - * Provided: - * source code - * source code system - * target code system #2 - */ - TranslationRequest translationRequest = new TranslationRequest(); - translationRequest.getCodeableConcept().addCoding() - .setSystem(CS_URL) - .setCode("12345"); - translationRequest.setTargetSystem(new UriType(CS_URL_2)); - - TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); - - assertTrue(translationResult.getResult().booleanValue()); - assertEquals("Matches found!", translationResult.getMessage().getValueAsString()); - - assertEquals(1, translationResult.getMatches().size()); - - TranslationMatch translationMatch = translationResult.getMatches().get(0); - assertEquals(ConceptMapEquivalence.EQUAL.toCode(), translationMatch.getEquivalence().getCode()); - Coding concept = translationMatch.getConcept(); - assertEquals("34567", concept.getCode()); - assertEquals("Target Code 34567", concept.getDisplay()); - assertEquals(CS_URL_2, concept.getSystem()); - assertEquals("Version 2", concept.getVersion()); - assertFalse(concept.getUserSelected()); - assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); - } - }); - } - - @Test - public void testTranslateUsingPredicatesWithSourceAndTargetSystem3() { - ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); - - ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); - - new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus theStatus) { - /* - * Provided: - * source code - * source code system - * target code system #3 - */ - TranslationRequest translationRequest = new TranslationRequest(); - translationRequest.getCodeableConcept().addCoding() - .setSystem(CS_URL) - .setCode("12345"); - translationRequest.setTargetSystem(new UriType(CS_URL_3)); - - TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); - - assertTrue(translationResult.getResult().booleanValue()); - assertEquals("Matches found!", translationResult.getMessage().getValueAsString()); - - assertEquals(2, translationResult.getMatches().size()); - - TranslationMatch translationMatch = translationResult.getMatches().get(0); - assertEquals(ConceptMapEquivalence.EQUAL.toCode(), translationMatch.getEquivalence().getCode()); - Coding concept = translationMatch.getConcept(); - assertEquals("56789", concept.getCode()); - assertEquals("Target Code 56789", concept.getDisplay()); - assertEquals(CS_URL_3, concept.getSystem()); - assertEquals("Version 4", concept.getVersion()); - assertFalse(concept.getUserSelected()); - assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); - - translationMatch = translationResult.getMatches().get(1); - assertEquals(ConceptMapEquivalence.WIDER.toCode(), translationMatch.getEquivalence().getCode()); - concept = translationMatch.getConcept(); - assertEquals("67890", concept.getCode()); - assertEquals("Target Code 67890", concept.getDisplay()); - assertEquals(CS_URL_3, concept.getSystem()); - assertEquals("Version 4", concept.getVersion()); - assertFalse(concept.getUserSelected()); - assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); - } - }); - } - @Test public void testTranslateUsingPredicatesWithSourceValueSet() { ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); @@ -686,6 +702,92 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test { }); } + @Test + public void testTranslateWithReverseUsingPredicatesWithSourceAndTargetSystem1() { + ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); + + ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); + + new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus theStatus) { + /* + * Provided: + * source code + * source code system + * target code system #1 + * reverse = true + */ + TranslationRequest translationRequest = new TranslationRequest(); + translationRequest.getCodeableConcept().addCoding() + .setSystem(CS_URL_2) + .setCode("34567"); + translationRequest.setTargetSystem(new UriType(CS_URL)); + translationRequest.setReverse(true); + + TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); + + assertTrue(translationResult.getResult().booleanValue()); + assertEquals("Matches found!", translationResult.getMessage().getValueAsString()); + + assertEquals(1, translationResult.getMatches().size()); + + TranslationMatch translationMatch = translationResult.getMatches().get(0); + assertNull(translationMatch.getEquivalence()); + Coding concept = translationMatch.getConcept(); + assertEquals("12345", concept.getCode()); + assertEquals("Source Code 12345", concept.getDisplay()); + assertEquals(CS_URL, concept.getSystem()); + assertEquals("Version 1", concept.getVersion()); + assertFalse(concept.getUserSelected()); + assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); + } + }); + } + + @Test + public void testTranslateWithReverseUsingPredicatesWithSourceAndTargetSystem4() { + ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); + + ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); + + new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus theStatus) { + /* + * Provided: + * source code + * source code system + * target code system #4 + * reverse = true + */ + TranslationRequest translationRequest = new TranslationRequest(); + translationRequest.getCodeableConcept().addCoding() + .setSystem(CS_URL_2) + .setCode("34567"); + translationRequest.setTargetSystem(new UriType(CS_URL_4)); + translationRequest.setReverse(true); + + TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); + + assertTrue(translationResult.getResult().booleanValue()); + assertEquals("Matches found!", translationResult.getMessage().getValueAsString()); + + assertEquals(1, translationResult.getMatches().size()); + + TranslationMatch translationMatch = translationResult.getMatches().get(0); + assertNull(translationMatch.getEquivalence()); + Coding concept = translationMatch.getConcept(); + assertEquals("78901", concept.getCode()); + assertEquals("Source Code 78901", concept.getDisplay()); + assertEquals(CS_URL_4, concept.getSystem()); + assertEquals("Version 5", concept.getVersion()); + assertFalse(concept.getUserSelected()); + assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); + } + }); + } + @Test public void testTranslateWithReverseUsingPredicatesWithSourceSystem() { ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); @@ -790,92 +892,6 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test { }); } - @Test - public void testTranslateWithReverseUsingPredicatesWithSourceAndTargetSystem1() { - ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); - - ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); - - new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus theStatus) { - /* - * Provided: - * source code - * source code system - * target code system #1 - * reverse = true - */ - TranslationRequest translationRequest = new TranslationRequest(); - translationRequest.getCodeableConcept().addCoding() - .setSystem(CS_URL_2) - .setCode("34567"); - translationRequest.setTargetSystem(new UriType(CS_URL)); - translationRequest.setReverse(true); - - TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); - - assertTrue(translationResult.getResult().booleanValue()); - assertEquals("Matches found!", translationResult.getMessage().getValueAsString()); - - assertEquals(1, translationResult.getMatches().size()); - - TranslationMatch translationMatch = translationResult.getMatches().get(0); - assertNull(translationMatch.getEquivalence()); - Coding concept = translationMatch.getConcept(); - assertEquals("12345", concept.getCode()); - assertEquals("Source Code 12345", concept.getDisplay()); - assertEquals(CS_URL, concept.getSystem()); - assertEquals("Version 1", concept.getVersion()); - assertFalse(concept.getUserSelected()); - assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); - } - }); - } - - @Test - public void testTranslateWithReverseUsingPredicatesWithSourceAndTargetSystem4() { - ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); - - ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); - - new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus theStatus) { - /* - * Provided: - * source code - * source code system - * target code system #4 - * reverse = true - */ - TranslationRequest translationRequest = new TranslationRequest(); - translationRequest.getCodeableConcept().addCoding() - .setSystem(CS_URL_2) - .setCode("34567"); - translationRequest.setTargetSystem(new UriType(CS_URL_4)); - translationRequest.setReverse(true); - - TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null); - - assertTrue(translationResult.getResult().booleanValue()); - assertEquals("Matches found!", translationResult.getMessage().getValueAsString()); - - assertEquals(1, translationResult.getMatches().size()); - - TranslationMatch translationMatch = translationResult.getMatches().get(0); - assertNull(translationMatch.getEquivalence()); - Coding concept = translationMatch.getConcept(); - assertEquals("78901", concept.getCode()); - assertEquals("Source Code 78901", concept.getDisplay()); - assertEquals(CS_URL_4, concept.getSystem()); - assertEquals("Version 5", concept.getVersion()); - assertFalse(concept.getUserSelected()); - assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); - } - }); - } - @Test public void testTranslateWithReverseUsingPredicatesWithSourceValueSet() { ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); @@ -977,4 +993,9 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test { } }); } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } } 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 new file mode 100644 index 00000000000..0d6aebf7375 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -0,0 +1,109 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.util.TestUtil; +import net.ttddyy.dsproxy.QueryCountHolder; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.Patient; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; +import org.springframework.test.context.TestPropertySource; + +import static org.junit.Assert.*; + +@TestPropertySource(properties = { + "scheduling_disabled=true" +}) +public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class); + + @After + public void afterResetDao() { + myDaoConfig.setResourceMetaCountHardLimit(new DaoConfig().getResourceMetaCountHardLimit()); + myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); + } + + @Test + public void testCreateClientAssignedId() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + + QueryCountHolder.clear(); + ourLog.info("** Starting Update Non-Existing resource with client assigned ID"); + Patient p = new Patient(); + p.setId("A"); + p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field + myPatientDao.update(p).getId().toUnqualifiedVersionless(); + + assertEquals(1, QueryCountHolder.getGrandTotal().getSelect()); + assertEquals(4, QueryCountHolder.getGrandTotal().getInsert()); + assertEquals(0, QueryCountHolder.getGrandTotal().getDelete()); + // Because of the forced ID's bidirectional link HFJ_RESOURCE <-> HFJ_FORCED_ID + assertEquals(1, QueryCountHolder.getGrandTotal().getUpdate()); + runInTransaction(() -> { + assertEquals(1, myResourceTableDao.count()); + assertEquals(1, myResourceHistoryTableDao.count()); + assertEquals(1, myForcedIdDao.count()); + assertEquals(1, myResourceIndexedSearchParamTokenDao.count()); + }); + + // Ok how about an update + + QueryCountHolder.clear(); + ourLog.info("** Starting Update Existing resource with client assigned ID"); + p = new Patient(); + p.setId("A"); + p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field + myPatientDao.update(p).getId().toUnqualifiedVersionless(); + + assertEquals(5, QueryCountHolder.getGrandTotal().getSelect()); + assertEquals(1, QueryCountHolder.getGrandTotal().getInsert()); + assertEquals(0, QueryCountHolder.getGrandTotal().getDelete()); + assertEquals(1, QueryCountHolder.getGrandTotal().getUpdate()); + runInTransaction(() -> { + assertEquals(1, myResourceTableDao.count()); + assertEquals(2, myResourceHistoryTableDao.count()); + assertEquals(1, myForcedIdDao.count()); + assertEquals(1, myResourceIndexedSearchParamTokenDao.count()); + }); + + } + + + @Test + public void testOneRowPerUpdate() { + myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); + + QueryCountHolder.clear(); + Patient p = new Patient(); + p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field + IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); + + assertEquals(3, QueryCountHolder.getGrandTotal().getInsert()); + runInTransaction(() -> { + assertEquals(1, myResourceTableDao.count()); + assertEquals(1, myResourceHistoryTableDao.count()); + }); + + QueryCountHolder.clear(); + p = new Patient(); + p.setId(id); + p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field + myPatientDao.update(p).getId().toUnqualifiedVersionless(); + + assertEquals(1, QueryCountHolder.getGrandTotal().getInsert()); + runInTransaction(() -> { + assertEquals(1, myResourceTableDao.count()); + assertEquals(2, myResourceHistoryTableDao.count()); + }); + + } + + + @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 d6c93c27d97..df59b11e7f8 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 @@ -128,7 +128,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test } @Test - public void testCreateSearchParameterOnSearchParameterDoesntCauseEndlessReindexLoop() throws InterruptedException { + public void testCreateSearchParameterOnSearchParameterDoesntCauseEndlessReindexLoop() { SearchParameter fooSp = new SearchParameter(); fooSp.setCode("foo"); fooSp.addBase("SearchParameter"); @@ -355,7 +355,6 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test assertThat(results, contains(mrId)); } - /** * See #863 */ @@ -709,6 +708,12 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test @Override protected void doInTransactionWithoutResult(TransactionStatus theArg0) { mySearchParameterDao.create(siblingSp, mySrd); + } + }); + + txTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus theArg0) { mySearchParamRegsitry.forceRefresh(); } }); @@ -1032,6 +1037,48 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test } + @Test + public void testSearchParameterDescendsIntoContainedResource() { + SearchParameter sp = new SearchParameter(); + sp.addBase("Observation"); + sp.setCode("specimencollectedtime"); + sp.setType(Enumerations.SearchParamType.DATE); + sp.setTitle("Observation Specimen Collected Time"); + sp.setExpression("Observation.specimen.resolve().receivedTime"); + sp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(sp); + + mySearchParamRegsitry.forceRefresh(); + + Specimen specimen = new Specimen(); + specimen.setId("#FOO"); + specimen.setReceivedTimeElement(new DateTimeType("2011-01-01")); + Observation o = new Observation(); + o.setId("O1"); + o.getContained().add(specimen); + o.setStatus(Observation.ObservationStatus.FINAL); + o.setSpecimen(new Reference("#FOO")); + myObservationDao.update(o); + + specimen = new Specimen(); + specimen.setId("#FOO"); + specimen.setReceivedTimeElement(new DateTimeType("2011-01-03")); + o = new Observation(); + o.setId("O2"); + o.getContained().add(specimen); + o.setStatus(Observation.ObservationStatus.FINAL); + o.setSpecimen(new Reference("#FOO")); + myObservationDao.update(o); + + SearchParameterMap params = new SearchParameterMap(); + params.add("specimencollectedtime", new DateParam("2011-01-01")); + IBundleProvider outcome = myObservationDao.search(params); + List ids = toUnqualifiedVersionlessIdValues(outcome); + ourLog.info("IDS: " + ids); + assertThat(ids, contains("Observation/O1")); + } + @Test public void testSearchWithCustomParam() { 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 43cc2eae363..ea95aa93052 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 @@ -46,7 +46,6 @@ public class FhirResourceDaoR4SearchMissingTest extends BaseJpaR4Test { org.setActive(true); myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); - assertThat(mySearchParamDao.findAll(), empty()); assertThat(mySearchParamPresentDao.findAll(), empty()); assertThat(myResourceIndexedSearchParamStringDao.findAll(), empty()); assertThat(myResourceIndexedSearchParamDateDao.findAll(), empty()); 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 e340b51166f..e13abc0b5a5 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 @@ -485,11 +485,18 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { Class type = ResourceIndexedSearchParamNumber.class; List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); ourLog.info(toStringMultiline(results)); - assertThat(results, containsInAnyOrder( - ((ResourceIndexedSearchParamNumber) (new ResourceIndexedSearchParamNumber(ImmunizationRecommendation.SP_DOSE_SEQUENCE, null).setResource(resource).setMissing(true))), - ((ResourceIndexedSearchParamNumber) (new ResourceIndexedSearchParamNumber(ImmunizationRecommendation.SP_DOSE_NUMBER, new BigDecimal("1.00")).setResource(resource))), - ((ResourceIndexedSearchParamNumber) (new ResourceIndexedSearchParamNumber(ImmunizationRecommendation.SP_DOSE_NUMBER, new BigDecimal("2.00")).setResource(resource))) - )); + + ResourceIndexedSearchParamNumber expect0 = new ResourceIndexedSearchParamNumber(ImmunizationRecommendation.SP_DOSE_NUMBER, new BigDecimal("2.00")); + expect0.setResource(resource); + expect0.calculateHashes(); + ResourceIndexedSearchParamNumber expect1 = new ResourceIndexedSearchParamNumber(ImmunizationRecommendation.SP_DOSE_SEQUENCE, null); + expect1.setResource(resource).setMissing(true); + expect1.calculateHashes(); + ResourceIndexedSearchParamNumber expect2 = new ResourceIndexedSearchParamNumber(ImmunizationRecommendation.SP_DOSE_NUMBER, new BigDecimal("1.00")); + expect2.setResource(resource); + expect2.calculateHashes(); + + assertThat(results, containsInAnyOrder(expect0, expect1, expect2)); } }); } @@ -504,10 +511,12 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { IIdType id = mySubstanceDao.create(res, mySrd).getId().toUnqualifiedVersionless(); - Class type = ResourceIndexedSearchParamQuantity.class; - List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); - ourLog.info(toStringMultiline(results)); - assertEquals(2, results.size()); + runInTransaction(()->{ + Class type = ResourceIndexedSearchParamQuantity.class; + List results = myEntityManager.createQuery("SELECT i FROM " + type.getSimpleName() + " i", type).getResultList(); + ourLog.info(toStringMultiline(results)); + assertEquals(2, results.size()); + }); List actual = toUnqualifiedVersionlessIds( mySubstanceDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Substance.SP_QUANTITY, new QuantityParam(null, 123, "http://foo", "UNIT")))); @@ -2261,6 +2270,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { @Test public void testSearchWithContains() { + myDaoConfig.setAllowContainsSearches(true); Patient pt1 = new Patient(); pt1.addName().setFamily("ABCDEFGHIJK"); 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 f43abf9f44c..bce25c41bbf 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 @@ -1,30 +1,13 @@ package ca.uhn.fhir.jpa.dao.r4; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.containsStringIgnoringCase; -import static org.hamcrest.Matchers.empty; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - -import java.util.*; - -import ca.uhn.fhir.jpa.term.BaseHapiTerminologySvcImpl; -import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.model.AllergyIntolerance.AllergyIntoleranceCategory; -import org.hl7.fhir.r4.model.AllergyIntolerance.AllergyIntoleranceClinicalStatus; -import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; -import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; -import org.hl7.fhir.r4.model.ValueSet.*; -import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.*; -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.entity.*; +import ca.uhn.fhir.jpa.entity.ResourceTable; +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.BaseHapiTerminologySvcImpl; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.param.TokenParam; @@ -34,20 +17,36 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.AllergyIntolerance.AllergyIntoleranceCategory; +import org.hl7.fhir.r4.model.AllergyIntolerance.AllergyIntoleranceClinicalStatus; +import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; +import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.r4.model.ValueSet.*; +import org.junit.*; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4TerminologyTest.class); public static final String URL_MY_CODE_SYSTEM = "http://example.com/my_code_system"; public static final String URL_MY_VALUE_SET = "http://example.com/my_value_set"; - + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4TerminologyTest.class); @Autowired private IHapiTerminologySvc myHapiTerminologySvc; @After public void after() { myDaoConfig.setDeferIndexingForCodesystemsOfSize(new DaoConfig().getDeferIndexingForCodesystemsOfSize()); - + BaseHapiTerminologySvcImpl.setForceSaveDeferredAlwaysForUnitTest(false); } @@ -96,38 +95,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { TermConcept childCA = new TermConcept(cs, "childCA").setDisplay("Child CA"); parentC.addChild(childCA, RelationshipTypeEnum.ISA); - myTermSvc.storeNewCodeSystemVersion(table.getId(), URL_MY_CODE_SYSTEM,"SYSTEM NAME" , cs); - return codeSystem; - } - - private CodeSystem createExternalCsLarge() { - CodeSystem codeSystem = new CodeSystem(); - codeSystem.setUrl(URL_MY_CODE_SYSTEM); - codeSystem.setContent(CodeSystemContentMode.NOTPRESENT); - IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); - - ResourceTable table = myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new); - - TermCodeSystemVersion cs = new TermCodeSystemVersion(); - cs.setResource(table); - - TermConcept parentA = new TermConcept(cs, "codeA").setDisplay("CodeA"); - cs.getConcepts().add(parentA); - - for (int i = 0; i < 450; i++) { - TermConcept childI = new TermConcept(cs, "subCodeA"+i).setDisplay("Sub-code A"+i); - parentA.addChild(childI, RelationshipTypeEnum.ISA); - } - - TermConcept parentB = new TermConcept(cs, "codeB").setDisplay("CodeB"); - cs.getConcepts().add(parentB); - - for (int i = 0; i < 450; i++) { - TermConcept childI = new TermConcept(cs, "subCodeB"+i).setDisplay("Sub-code B"+i); - parentB.addChild(childI, RelationshipTypeEnum.ISA); - } - - myTermSvc.storeNewCodeSystemVersion(table.getId(), URL_MY_CODE_SYSTEM,"SYSTEM NAME" , cs); + myTermSvc.storeNewCodeSystemVersion(table.getId(), URL_MY_CODE_SYSTEM, "SYSTEM NAME", cs); return codeSystem; } @@ -153,17 +121,48 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { TermConcept goodbye = new TermConcept(cs, "goodbye").setDisplay("Goodbye"); cs.getConcepts().add(goodbye); - + TermConcept dogs = new TermConcept(cs, "dogs").setDisplay("Dogs"); cs.getConcepts().add(dogs); - + TermConcept labrador = new TermConcept(cs, "labrador").setDisplay("Labrador"); dogs.addChild(labrador, RelationshipTypeEnum.ISA); TermConcept beagle = new TermConcept(cs, "beagle").setDisplay("Beagle"); dogs.addChild(beagle, RelationshipTypeEnum.ISA); - myTermSvc.storeNewCodeSystemVersion(table.getId(), URL_MY_CODE_SYSTEM,"SYSTEM NAME" , cs); + myTermSvc.storeNewCodeSystemVersion(table.getId(), URL_MY_CODE_SYSTEM, "SYSTEM NAME", cs); + return codeSystem; + } + + private CodeSystem createExternalCsLarge() { + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setUrl(URL_MY_CODE_SYSTEM); + codeSystem.setContent(CodeSystemContentMode.NOTPRESENT); + IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); + + ResourceTable table = myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new); + + TermCodeSystemVersion cs = new TermCodeSystemVersion(); + cs.setResource(table); + + TermConcept parentA = new TermConcept(cs, "codeA").setDisplay("CodeA"); + cs.getConcepts().add(parentA); + + for (int i = 0; i < 450; i++) { + TermConcept childI = new TermConcept(cs, "subCodeA" + i).setDisplay("Sub-code A" + i); + parentA.addChild(childI, RelationshipTypeEnum.ISA); + } + + TermConcept parentB = new TermConcept(cs, "codeB").setDisplay("CodeB"); + cs.getConcepts().add(parentB); + + for (int i = 0; i < 450; i++) { + TermConcept childI = new TermConcept(cs, "subCodeB" + i).setDisplay("Sub-code B" + i); + parentB.addChild(childI, RelationshipTypeEnum.ISA); + } + + myTermSvc.storeNewCodeSystemVersion(table.getId(), URL_MY_CODE_SYSTEM, "SYSTEM NAME", cs); return codeSystem; } @@ -171,17 +170,17 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { //@formatter:off CodeSystem codeSystem = new CodeSystem(); codeSystem.setUrl(URL_MY_CODE_SYSTEM); - codeSystem.setContent(CodeSystemContentMode.COMPLETE); + codeSystem.setContent(CodeSystemContentMode.COMPLETE); codeSystem .addConcept().setCode("A").setDisplay("Code A") - .addConcept(new ConceptDefinitionComponent().setCode("AA").setDisplay("Code AA") - .addConcept(new ConceptDefinitionComponent().setCode("AAA").setDisplay("Code AAA")) - ) - .addConcept(new ConceptDefinitionComponent().setCode("AB").setDisplay("Code AB")); + .addConcept(new ConceptDefinitionComponent().setCode("AA").setDisplay("Code AA") + .addConcept(new ConceptDefinitionComponent().setCode("AAA").setDisplay("Code AAA")) + ) + .addConcept(new ConceptDefinitionComponent().setCode("AB").setDisplay("Code AB")); codeSystem .addConcept().setCode("B").setDisplay("Code B") - .addConcept(new ConceptDefinitionComponent().setCode("BA").setDisplay("Code BA")) - .addConcept(new ConceptDefinitionComponent().setCode("BB").setDisplay("Code BB")); + .addConcept(new ConceptDefinitionComponent().setCode("BA").setDisplay("Code BA")) + .addConcept(new ConceptDefinitionComponent().setCode("BB").setDisplay("Code BB")); //@formatter:on myCodeSystemDao.create(codeSystem, mySrd); @@ -234,15 +233,15 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { //@formatter:off CodeSystem codeSystem = new CodeSystem(); codeSystem.setUrl(URL_MY_CODE_SYSTEM); - codeSystem.setContent(CodeSystemContentMode.COMPLETE); + codeSystem.setContent(CodeSystemContentMode.COMPLETE); codeSystem .addConcept().setCode("A").setDisplay("Code A") - .addConcept(new ConceptDefinitionComponent().setCode("AA").setDisplay("Code AA")) - .addConcept(new ConceptDefinitionComponent().setCode("AB").setDisplay("Code AB")); + .addConcept(new ConceptDefinitionComponent().setCode("AA").setDisplay("Code AA")) + .addConcept(new ConceptDefinitionComponent().setCode("AB").setDisplay("Code AB")); codeSystem .addConcept().setCode("B").setDisplay("Code A") - .addConcept(new ConceptDefinitionComponent().setCode("BA").setDisplay("Code AA")) - .addConcept(new ConceptDefinitionComponent().setCode("BB").setDisplay("Code AB")); + .addConcept(new ConceptDefinitionComponent().setCode("BA").setDisplay("Code AA")) + .addConcept(new ConceptDefinitionComponent().setCode("BB").setDisplay("Code AB")); //@formatter:on IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); @@ -252,6 +251,20 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { } + @Test + public void testConceptTimestamps() { + long start = System.currentTimeMillis() - 10; + + createExternalCsDogs(); + + runInTransaction(() -> { + List concepts = myTermConceptDao.findAll(); + for (TermConcept next : concepts) { + assertTrue(next.getUpdated().getTime() > start); + } + }); + } + @Test public void testExpandInvalid() { createExternalCsAndLocalVs(); @@ -278,17 +291,17 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { valueSet.setUrl(URL_MY_VALUE_SET); valueSet.getCompose() .addInclude() - .setSystem(codeSystem.getUrl()) - .addConcept(new ConceptReferenceComponent().setCode("hello")) - .addConcept(new ConceptReferenceComponent().setCode("goodbye")); + .setSystem(codeSystem.getUrl()) + .addConcept(new ConceptReferenceComponent().setCode("hello")) + .addConcept(new ConceptReferenceComponent().setCode("goodbye")); valueSet.getCompose() .addInclude() - .setSystem(codeSystem.getUrl()) - .addFilter() - .setProperty("concept") - .setOp(FilterOperator.ISA) - .setValue("dogs"); - + .setSystem(codeSystem.getUrl()) + .addFilter() + .setProperty("concept") + .setOp(FilterOperator.ISA) + .setValue("dogs"); + myValueSetDao.create(valueSet, mySrd); ValueSet result = myValueSetDao.expand(valueSet, ""); @@ -300,47 +313,6 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { } - // TODO: get this working - @Ignore - @Test - public void testExpandWithOpEquals() { - - - ValueSet result = myValueSetDao.expandByIdentifier("http://hl7.org/fhir/ValueSet/doc-typecodes", ""); - ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result)); - } - - - @Test - public void testExpandWithCodesAndDisplayFilterPartialOnFilter() { - CodeSystem codeSystem = createExternalCsDogs(); - - ValueSet valueSet = new ValueSet(); - valueSet.setUrl(URL_MY_VALUE_SET); - valueSet.getCompose() - .addInclude() - .setSystem(codeSystem.getUrl()) - .addConcept(new ConceptReferenceComponent().setCode("hello")) - .addConcept(new ConceptReferenceComponent().setCode("goodbye")); - valueSet.getCompose() - .addInclude() - .setSystem(codeSystem.getUrl()) - .addFilter() - .setProperty("concept") - .setOp(FilterOperator.ISA) - .setValue("dogs"); - - myValueSetDao.create(valueSet, mySrd); - - ValueSet result = myValueSetDao.expand(valueSet, "lab"); - logAndValidateValueSet(result); - - assertEquals(1, result.getExpansion().getTotal()); - ArrayList codes = toCodesContains(result.getExpansion().getContains()); - assertThat(codes, containsInAnyOrder("labrador")); - - } - @Test public void testExpandWithCodesAndDisplayFilterPartialOnCodes() { CodeSystem codeSystem = createExternalCsDogs(); @@ -349,17 +321,17 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { valueSet.setUrl(URL_MY_VALUE_SET); valueSet.getCompose() .addInclude() - .setSystem(codeSystem.getUrl()) - .addConcept(new ConceptReferenceComponent().setCode("hello")) - .addConcept(new ConceptReferenceComponent().setCode("goodbye")); + .setSystem(codeSystem.getUrl()) + .addConcept(new ConceptReferenceComponent().setCode("hello")) + .addConcept(new ConceptReferenceComponent().setCode("goodbye")); valueSet.getCompose() .addInclude() - .setSystem(codeSystem.getUrl()) - .addFilter() - .setProperty("concept") - .setOp(FilterOperator.ISA) - .setValue("dogs"); - + .setSystem(codeSystem.getUrl()) + .addFilter() + .setProperty("concept") + .setOp(FilterOperator.ISA) + .setValue("dogs"); + myValueSetDao.create(valueSet, mySrd); ValueSet result = myValueSetDao.expand(valueSet, "hel"); @@ -389,6 +361,36 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { } + @Test + public void testExpandWithCodesAndDisplayFilterPartialOnFilter() { + CodeSystem codeSystem = createExternalCsDogs(); + + ValueSet valueSet = new ValueSet(); + valueSet.setUrl(URL_MY_VALUE_SET); + valueSet.getCompose() + .addInclude() + .setSystem(codeSystem.getUrl()) + .addConcept(new ConceptReferenceComponent().setCode("hello")) + .addConcept(new ConceptReferenceComponent().setCode("goodbye")); + valueSet.getCompose() + .addInclude() + .setSystem(codeSystem.getUrl()) + .addFilter() + .setProperty("concept") + .setOp(FilterOperator.ISA) + .setValue("dogs"); + + myValueSetDao.create(valueSet, mySrd); + + ValueSet result = myValueSetDao.expand(valueSet, "lab"); + logAndValidateValueSet(result); + + assertEquals(1, result.getExpansion().getTotal()); + ArrayList codes = toCodesContains(result.getExpansion().getContains()); + assertThat(codes, containsInAnyOrder("labrador")); + + } + @Test public void testExpandWithDisplayInExternalValueSetFuzzyMatching() { createExternalCsAndLocalVs(); @@ -442,6 +444,56 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { assertThat(codes, containsInAnyOrder("ParentA", "ParentB", "childAB", "childAAB", "ParentC", "childBA", "childCA")); } + @Test + public void testExpandWithIncludeContainingDashesInInclude() { + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setUrl(URL_MY_CODE_SYSTEM); + codeSystem.setContent(CodeSystemContentMode.NOTPRESENT); + IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); + + ResourceTable table = myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new); + + TermCodeSystemVersion cs = new TermCodeSystemVersion(); + cs.setResource(table); + + TermConcept concept; + concept = new TermConcept(cs, "LA1111-2"); + cs.getConcepts().add(concept); + concept = new TermConcept(cs, "LA2222-2"); + cs.getConcepts().add(concept); + concept = new TermConcept(cs, "LA3333-2"); + cs.getConcepts().add(concept); + concept = new TermConcept(cs, "LA1122-2"); + cs.getConcepts().add(concept); + concept = new TermConcept(cs, "LA1133-2"); + cs.getConcepts().add(concept); + concept = new TermConcept(cs, "LA4444-2"); + cs.getConcepts().add(concept); + concept = new TermConcept(cs, "LA9999-7"); + cs.getConcepts().add(concept); + + myTermSvc.storeNewCodeSystemVersion(table.getId(), URL_MY_CODE_SYSTEM, "SYSTEM NAME", cs); + + ValueSet valueSet = new ValueSet(); + valueSet.setUrl(URL_MY_VALUE_SET); + valueSet.getCompose() + .addInclude() + .setSystem(codeSystem.getUrl()) + .addConcept(new ConceptReferenceComponent().setCode("LA2222-2")) + .addConcept(new ConceptReferenceComponent().setCode("LA1122-2")); + IIdType vsid = myValueSetDao.create(valueSet, mySrd).getId().toUnqualifiedVersionless(); + + ValueSet expansion = myValueSetDao.expand(vsid, null, null); + Set codes = expansion + .getExpansion() + .getContains() + .stream() + .map(t -> t.getCode()) + .collect(Collectors.toSet()); + ourLog.info("Codes: {}", codes); + assertThat(codes, containsInAnyOrder("LA2222-2", "LA1122-2")); + } + @Test public void testExpandWithInvalidExclude() { createExternalCsAndLocalVs(); @@ -484,7 +536,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { @Test public void testExpandWithIsAInExternalValueSetReindex() { BaseHapiTerminologySvcImpl.setForceSaveDeferredAlwaysForUnitTest(true); - + createExternalCsAndLocalVs(); mySystemDao.markAllResourcesForReindexing(); @@ -494,7 +546,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { myHapiTerminologySvc.saveDeferred(); myHapiTerminologySvc.saveDeferred(); myHapiTerminologySvc.saveDeferred(); - + ValueSet vs = new ValueSet(); ConceptSetComponent include = vs.getCompose().addInclude(); include.setSystem(URL_MY_CODE_SYSTEM); @@ -542,7 +594,17 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { assertEquals("unable to find code system http://example.com/my_code_systemAA", e.getMessage()); } } - + + // TODO: get this working + @Ignore + @Test + public void testExpandWithOpEquals() { + + + ValueSet result = myValueSetDao.expandByIdentifier("http://hl7.org/fhir/ValueSet/doc-typecodes", ""); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result)); + } + @Test public void testExpandWithSystemAndCodesAndFilterKeywordInLocalValueSet() { createLocalCsAndVs(); @@ -555,12 +617,12 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { include.addFilter().setProperty("display").setOp(FilterOperator.EQUAL).setValue("AAA"); ValueSet result = myValueSetDao.expand(vs, null); - + // Technically it's not valid to expand a ValueSet with both includes and filters so the // result fails validation because of the input.. we're being permissive by allowing both // though, so we won't validate the input result.setCompose(new ValueSetComposeComponent()); - + logAndValidateValueSet(result); ArrayList codes = toCodesContains(result.getExpansion().getContains()); @@ -631,7 +693,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { valueSet.setUrl(URL_MY_VALUE_SET); valueSet.getCompose() .addInclude() - .setSystem(codeSystem.getUrl()); + .setSystem(codeSystem.getUrl()); ValueSet result = myValueSetDao.expand(valueSet, ""); logAndValidateValueSet(result); @@ -713,7 +775,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { cs.setResource(table); TermConcept parentA = new TermConcept(cs, "ParentA").setDisplay("Parent A"); cs.getConcepts().add(parentA); - myTermSvc.storeNewCodeSystemVersion(table.getId(), "http://snomed.info/sct","Snomed CT" , cs); + myTermSvc.storeNewCodeSystemVersion(table.getId(), "http://snomed.info/sct", "Snomed CT", cs); StringType code = new StringType("ParentA"); StringType system = new StringType("http://snomed.info/sct"); @@ -769,7 +831,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { myTermSvc.saveDeferred(); mySystemDao.performReindexingPass(null); myTermSvc.saveDeferred(); - + // Again mySystemDao.markAllResourcesForReindexing(); mySystemDao.performReindexingPass(null); @@ -818,19 +880,6 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { } - @Test - public void testSearchCodeInUnknownCodeSystem() { - - SearchParameterMap params = new SearchParameterMap(); - - try { - params.add(Observation.SP_CODE, new TokenParam(null, URL_MY_VALUE_SET).setModifier(TokenParamModifier.IN)); - assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), empty()); - } catch (InvalidRequestException e) { - assertEquals("Unable to find imported value set http://example.com/my_value_set", e.getMessage()); - } - } - @Test public void testSearchCodeBelowBuiltInCodesystem() { AllergyIntolerance ai1 = new AllergyIntolerance(); @@ -918,33 +967,6 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { } - - @Test - public void testSearchCodeBelowLocalCodesystem() { - createLocalCsAndVs(); - - Observation obsAA = new Observation(); - obsAA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("AA"); - IIdType idAA = myObservationDao.create(obsAA, mySrd).getId().toUnqualifiedVersionless(); - - Observation obsBA = new Observation(); - obsBA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("BA"); - IIdType idBA = myObservationDao.create(obsBA, mySrd).getId().toUnqualifiedVersionless(); - - Observation obsCA = new Observation(); - obsCA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("CA"); - IIdType idCA = myObservationDao.create(obsCA, mySrd).getId().toUnqualifiedVersionless(); - - SearchParameterMap params = new SearchParameterMap(); - params.add(Observation.SP_CODE, new TokenParam(URL_MY_CODE_SYSTEM, "A").setModifier(TokenParamModifier.BELOW)); - assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), containsInAnyOrder(idAA.getValue())); - - params = new SearchParameterMap(); - params.add(Observation.SP_CODE, new TokenParam(URL_MY_CODE_SYSTEM, "AAA").setModifier(TokenParamModifier.BELOW)); - assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), empty()); - - } - @Test public void testSearchCodeBelowExternalCodesystemLarge() { createExternalCsLarge(); @@ -975,6 +997,32 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { } + @Test + public void testSearchCodeBelowLocalCodesystem() { + createLocalCsAndVs(); + + Observation obsAA = new Observation(); + obsAA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("AA"); + IIdType idAA = myObservationDao.create(obsAA, mySrd).getId().toUnqualifiedVersionless(); + + Observation obsBA = new Observation(); + obsBA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("BA"); + IIdType idBA = myObservationDao.create(obsBA, mySrd).getId().toUnqualifiedVersionless(); + + Observation obsCA = new Observation(); + obsCA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("CA"); + IIdType idCA = myObservationDao.create(obsCA, mySrd).getId().toUnqualifiedVersionless(); + + SearchParameterMap params = new SearchParameterMap(); + params.add(Observation.SP_CODE, new TokenParam(URL_MY_CODE_SYSTEM, "A").setModifier(TokenParamModifier.BELOW)); + assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), containsInAnyOrder(idAA.getValue())); + + params = new SearchParameterMap(); + params.add(Observation.SP_CODE, new TokenParam(URL_MY_CODE_SYSTEM, "AAA").setModifier(TokenParamModifier.BELOW)); + assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), empty()); + + } + @Test public void testSearchCodeInBuiltInValueSet() { AllergyIntolerance ai1 = new AllergyIntolerance(); @@ -1019,7 +1067,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { SearchParameterMap params; ourLog.info("testSearchCodeInEmptyValueSet without status"); - + params = new SearchParameterMap(); params.add(Observation.SP_CODE, new TokenParam(null, URL_MY_VALUE_SET).setModifier(TokenParamModifier.IN)); assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), empty()); @@ -1030,7 +1078,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { params.add(Observation.SP_CODE, new TokenParam(null, URL_MY_VALUE_SET).setModifier(TokenParamModifier.IN)); params.add(Observation.SP_STATUS, new TokenParam(null, "final")); assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), empty()); - + ourLog.info("testSearchCodeInEmptyValueSet done"); } @@ -1093,7 +1141,6 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { assertThat(toUnqualifiedVersionlessIdValues(myAuditEventDao.search(params)), empty()); } - @Test public void testSearchCodeInLocalCodesystem() { createLocalCsAndVs(); @@ -1116,6 +1163,19 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { } + @Test + public void testSearchCodeInUnknownCodeSystem() { + + SearchParameterMap params = new SearchParameterMap(); + + try { + params.add(Observation.SP_CODE, new TokenParam(null, URL_MY_VALUE_SET).setModifier(TokenParamModifier.IN)); + assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), empty()); + } catch (InvalidRequestException e) { + assertEquals("Unable to find imported value set http://example.com/my_value_set", e.getMessage()); + } + } + @Test public void testSearchCodeInValueSetThatImportsInvalidCodeSystem() { ValueSet valueSet = new ValueSet(); @@ -1126,12 +1186,12 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { SearchParameterMap params; ourLog.info("testSearchCodeInEmptyValueSet without status"); - + params = new SearchParameterMap(); params.add(Observation.SP_CODE, new TokenParam(null, URL_MY_VALUE_SET).setModifier(TokenParamModifier.IN)); try { myObservationDao.search(params); - } catch(InvalidRequestException e) { + } catch (InvalidRequestException e) { assertEquals("Unable to expand imported value set: Unable to find imported value set http://non_existant_VS", e.getMessage()); } 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 5144f0fcfa7..20e3dfc0f09 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,10 +1,7 @@ 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.ResourceHistoryTable; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; @@ -152,6 +149,42 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { return retVal; } + + @Test + public void testDeletedResourcesAreReindexed() { + 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)); + }); + + runInTransaction(()->{ + Optional tableOpt = myResourceTableDao.findById(id1.getIdPartAsLong()); + assertTrue(tableOpt.isPresent()); + ResourceTable table = tableOpt.get(); + table.setIndexStatus(null); + table.setDeleted(new Date()); + }); + + mySystemDao.performReindexingPass(1000); + mySystemDao.performReindexingPass(1000); + + 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() { String methodName = "testCantSearchForDeletedResourceByLanguageOrTag"; 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 89442af5487..ea57f398263 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java @@ -1,9 +1,11 @@ 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.SearchBuilder; import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.util.JpaConstants; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -27,6 +29,7 @@ import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.Nonnull; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.UUID; import static org.hamcrest.Matchers.*; 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 632dd8bfa86..9f727ea1aa4 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 @@ -39,9 +39,7 @@ public class FhirResourceDaoR4UpdateTagSnapshotTest extends BaseJpaR4Test { myPatientDao.update(p, mySrd); p = myPatientDao.read(new IdType("A"), mySrd); - // It would be nice if this didn't trigger a version update but - // i guess it's not so bad that it does - assertEquals("2", p.getIdElement().getVersionIdPart()); + assertEquals("1", p.getIdElement().getVersionIdPart()); assertEquals(true, p.getActive()); assertEquals(1, p.getMeta().getTag().size()); } @@ -86,9 +84,7 @@ public class FhirResourceDaoR4UpdateTagSnapshotTest extends BaseJpaR4Test { myPatientDao.update(p, mySrd); p = myPatientDao.read(new IdType("A"), mySrd); - // It would be nice if this didn't trigger a version update but - // i guess it's not so bad that it does - assertEquals("2", p.getIdElement().getVersionIdPart()); + assertEquals("1", p.getIdElement().getVersionIdPart()); assertEquals(true, p.getActive()); assertEquals(1, p.getMeta().getTag().size()); assertEquals("urn:foo", p.getMeta().getTag().get(0).getSystem()); @@ -136,9 +132,7 @@ public class FhirResourceDaoR4UpdateTagSnapshotTest extends BaseJpaR4Test { p = myPatientDao.read(new IdType("A"), mySrd); assertEquals(true, p.getActive()); assertEquals(0, p.getMeta().getTag().size()); - // It would be nice if this didn't trigger a version update but - // i guess it's not so bad that it does - assertEquals("2", p.getIdElement().getVersionIdPart()); + assertEquals("1", p.getIdElement().getVersionIdPart()); } @AfterClass 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 c3e60059bd8..64664d9e64d 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 @@ -25,7 +25,11 @@ import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.util.TestUtil; +import org.springframework.test.context.TestPropertySource; +@TestPropertySource(properties = { + "scheduling_disabled=true" +}) public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4UpdateTest.class); @@ -679,6 +683,8 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { ourLog.info("Now have {} inserts", QueryCountHolder.getGrandTotal().getInsert()); QueryCountHolder.clear(); + ourLog.info("** About to update"); + pt.setId(id); pt.getNameFirstRep().addGiven("GIVEN1C"); myPatientDao.update(pt); 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 fdd027d28fc..3d43fd835c6 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,6 +3,7 @@ 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.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; @@ -80,7 +81,7 @@ public class SearchParamExtractorR4Test { } }; - SearchParamExtractorR4 extractor = new SearchParamExtractorR4(ourCtx, ourValidationSupport, searchParamRegistry); + SearchParamExtractorR4 extractor = new SearchParamExtractorR4(new DaoConfig(), ourCtx, ourValidationSupport, searchParamRegistry); Set tokens = extractor.extractSearchParamTokens(new ResourceTable(), obs); assertEquals(1, tokens.size()); ResourceIndexedSearchParamToken token = (ResourceIndexedSearchParamToken) tokens.iterator().next(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantityTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantityTest.java index bb6a3cb8383..642820ee03d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantityTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamQuantityTest.java @@ -19,19 +19,8 @@ public class ResourceIndexedSearchParamQuantityTest { ResourceIndexedSearchParamQuantity token = createParam("NAME", "123.001", "value", "VALUE"); // Make sure our hashing function gives consistent results - assertEquals(945335027461836896L, token.getHashUnitsAndValPrefix().longValue()); - assertEquals(5549105497508660145L, token.getHashValPrefix().longValue()); - } - - @Test - public void testValueTrimming() { - assertEquals(7265149425397186226L, createParam("NAME", "401.001", "value", "VALUE").getHashUnitsAndValPrefix().longValue()); - assertEquals(7265149425397186226L, createParam("NAME", "401.99999", "value", "VALUE").getHashUnitsAndValPrefix().longValue()); - assertEquals(7265149425397186226L, createParam("NAME", "401", "value", "VALUE").getHashUnitsAndValPrefix().longValue()); - // Should be different - assertEquals(-8387917096585386046L, createParam("NAME", "400.9999999", "value", "VALUE").getHashUnitsAndValPrefix().longValue()); - // Should be different - assertEquals(8819656626732693650L, createParam("NAME", "402.000000", "value", "VALUE").getHashUnitsAndValPrefix().longValue()); + assertEquals(834432764963581074L, token.getHashIdentity().longValue()); + assertEquals(-1970227166134682431L, token.getHashIdentityAndUnits().longValue()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamStringTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamStringTest.java index 15cc5527ef2..403cf937850 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamStringTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamStringTest.java @@ -1,14 +1,16 @@ package ca.uhn.fhir.jpa.entity; +import ca.uhn.fhir.jpa.dao.DaoConfig; import org.junit.Test; import static org.junit.Assert.*; +@SuppressWarnings("SpellCheckingInspection") public class ResourceIndexedSearchParamStringTest { @Test public void testHashFunctions() { - ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString("NAME", "value", "VALUE"); + ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString(new DaoConfig(), "NAME", "value", "VALUE"); token.setResource(new ResourceTable().setResourceType("Patient")); // Make sure our hashing function gives consistent results @@ -18,7 +20,7 @@ public class ResourceIndexedSearchParamStringTest { @Test public void testHashFunctionsPrefixOnly() { - ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString("NAME", "vZZZZZZZZZZZZZZZZ", "VZZZZZZzzzZzzzZ"); + ResourceIndexedSearchParamString token = new ResourceIndexedSearchParamString(new DaoConfig(), "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/provider/ResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java index de2842a74e4..b3e2d9e7f87 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java @@ -61,6 +61,7 @@ import com.google.common.base.Charsets; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; +import ca.uhn.fhir.jpa.util.JpaConstants; import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; @@ -115,6 +116,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; + import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.NumberParam; import ca.uhn.fhir.rest.param.StringAndListParam; @@ -124,6 +126,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; + import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.TestUtil; @@ -162,11 +165,11 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { mySearchCoordinatorSvcRaw = AopTestUtils.getTargetObject(mySearchCoordinatorSvc); } - private void checkParamMissing(String paramName) throws IOException, ClientProtocolException { + private void checkParamMissing(String paramName) throws IOException { HttpGet get = new HttpGet(ourServerBase + "/Observation?" + paramName + ":missing=false"); - CloseableHttpResponse resp = ourHttpClient.execute(get); - IOUtils.closeQuietly(resp.getEntity().getContent()); - assertEquals(200, resp.getStatusLine().getStatusCode()); + try (CloseableHttpResponse resp = ourHttpClient.execute(get)) { + assertEquals(200, resp.getStatusLine().getStatusCode()); + } } /** @@ -261,7 +264,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testCountParam() throws Exception { + public void testCountParam() { // NB this does not get used- The paging provider has its own limits built in myDaoConfig.setHardSearchLimit(100); @@ -299,7 +302,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { * See #438 */ @Test - public void testCreateAndUpdateBinary() throws ClientProtocolException, Exception { + public void testCreateAndUpdateBinary() throws Exception { byte[] arr = {1, 21, 74, 123, -44}; Binary binary = new Binary(); binary.setContent(arr); @@ -364,7 +367,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testCreateQuestionnaireResponseWithValidation() throws IOException { + public void testCreateQuestionnaireResponseWithValidation() { ValueSet options = new ValueSet(); options.getCodeSystem().setSystem("urn:system").addConcept().setCode("code0"); IIdType optId = ourClient.create().resource(options).execute().getId(); @@ -517,6 +520,27 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } } + @Test + public void testCreateResourceWithNumericId() throws IOException { + String resource = ""; + + HttpPost post = new HttpPost(ourServerBase + "/Patient/2"); + post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + CloseableHttpResponse response = ourHttpClient.execute(post); + try { + String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseString); + assertEquals(400, response.getStatusLine().getStatusCode()); + OperationOutcome oo = myFhirCtx.newXmlParser().parseResource(OperationOutcome.class, responseString); + assertEquals("Can not create resource with ID \"2\", ID must not be supplied on a create (POST) operation (use an HTTP PUT / update operation if you wish to supply an ID)", + oo.getIssue().get(0).getDiagnostics()); + } finally { + response.getEntity().getContent().close(); + response.close(); + } + } + // private void delete(String theResourceType, String theParamName, String theParamValue) { // Bundle resources; // do { @@ -543,28 +567,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { // } @Test - public void testCreateResourceWithNumericId() throws IOException { - String resource = ""; - - HttpPost post = new HttpPost(ourServerBase + "/Patient/2"); - post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); - - CloseableHttpResponse response = ourHttpClient.execute(post); - try { - String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - ourLog.info(responseString); - assertEquals(400, response.getStatusLine().getStatusCode()); - OperationOutcome oo = myFhirCtx.newXmlParser().parseResource(OperationOutcome.class, responseString); - assertEquals("Can not create resource with ID \"2\", ID must not be supplied on a create (POST) operation (use an HTTP PUT / update operation if you wish to supply an ID)", - oo.getIssue().get(0).getDiagnostics()); - } finally { - response.getEntity().getContent().close(); - response.close(); - } - } - - @Test - public void testCreateWithForcedId() throws IOException { + public void testCreateWithForcedId() { String methodName = "testCreateWithForcedId"; Patient p = new Patient(); @@ -726,7 +729,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { * Based on email from Rene Spronk */ @Test - public void testDeleteResourceConditional2() throws IOException, Exception { + public void testDeleteResourceConditional2() throws Exception { String methodName = "testDeleteResourceConditional2"; Patient pt = new Patient(); @@ -793,7 +796,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { * See issue #52 */ @Test - public void testDiagnosticOrderResources() throws Exception { + public void testDiagnosticOrderResources() { IGenericClient client = ourClient; int initialSize = client @@ -885,7 +888,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testEverythingEncounterInstance() throws Exception { + public void testEverythingEncounterInstance() { String methodName = "testEverythingEncounterInstance"; Organization org1parent = new Organization(); @@ -949,7 +952,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testEverythingEncounterType() throws Exception { + public void testEverythingEncounterType() { String methodName = "testEverythingEncounterInstance"; Organization org1parent = new Organization(); @@ -1049,7 +1052,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { List actual; StringAndListParam param; - ourLog.info("Pt1:{} Pt2:{} Obs1:{} Obs2:{} Obs3:{}", new Object[] {ptId1.getIdPart(), ptId2.getIdPart(), obsId1.getIdPart(), obsId2.getIdPart(), obsId3.getIdPart()}); + ourLog.info("Pt1:{} Pt2:{} Obs1:{} Obs2:{} Obs3:{}", ptId1.getIdPart(), ptId2.getIdPart(), obsId1.getIdPart(), obsId2.getIdPart(), obsId3.getIdPart()); param = new StringAndListParam(); param.addAnd(new StringOrListParam().addOr(new StringParam("obsvalue1"))); @@ -1072,7 +1075,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { * See #147 */ @Test - public void testEverythingPatientDoesntRepeatPatient() throws Exception { + public void testEverythingPatientDoesntRepeatPatient() { ca.uhn.fhir.model.dstu2.resource.Bundle b; b = myFhirCtx.newJsonParser().parseResource(ca.uhn.fhir.model.dstu2.resource.Bundle.class, new InputStreamReader(ResourceProviderDstu2Test.class.getResourceAsStream("/bug147-bundle.json"))); @@ -1131,7 +1134,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { * Test for #226 */ @Test - public void testEverythingPatientIncludesBackReferences() throws Exception { + public void testEverythingPatientIncludesBackReferences() { String methodName = "testEverythingIncludesBackReferences"; Medication med = new Medication(); @@ -1158,7 +1161,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { * See #148 */ @Test - public void testEverythingPatientIncludesCondition() throws Exception { + public void testEverythingPatientIncludesCondition() { ca.uhn.fhir.model.dstu2.resource.Bundle b = new ca.uhn.fhir.model.dstu2.resource.Bundle(); Patient p = new Patient(); p.setId("1"); @@ -1190,7 +1193,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testEverythingPatientOperation() throws Exception { + public void testEverythingPatientOperation() { String methodName = "testEverythingOperation"; Organization org1parent = new Organization(); @@ -1235,7 +1238,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testEverythingPatientType() throws Exception { + public void testEverythingPatientType() { String methodName = "testEverythingPatientType"; Organization o1 = new Organization(); @@ -1549,7 +1552,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testMetaOperations() throws Exception { + public void testMetaOperations() { String methodName = "testMetaOperations"; Patient pt = new Patient(); @@ -1586,7 +1589,6 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } } - @Test public void testPagingOverEverythingSet() throws InterruptedException { Patient p = new Patient(); @@ -1641,7 +1643,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testPagingOverEverythingSetWithNoPagingProvider() throws InterruptedException { + public void testPagingOverEverythingSetWithNoPagingProvider() { ourRestServer.setPagingProvider(null); Patient p = new Patient(); @@ -1674,11 +1676,30 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } + @Test + public void testProcessMessage() { + + Bundle bundle = new Bundle(); + bundle.setType(BundleTypeEnum.MESSAGE); + + Parameters parameters = new Parameters(); + parameters.addParameter() + .setName("content") + .setResource(bundle); + try { + ourClient.operation().onType(MessageHeader.class).named(JpaConstants.OPERATION_PROCESS_MESSAGE).withParameters(parameters).execute(); + fail(); + } catch (NotImplementedOperationException e) { + assertThat(e.getMessage(), containsString("This operation is not yet implemented on this server")); + } + + } + /** * Test for issue #60 */ @Test - public void testReadAllInstancesOfType() throws Exception { + public void testReadAllInstancesOfType() { Patient pat; pat = new Patient(); @@ -2058,7 +2079,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } - private void testSearchReturnsResults(String search) throws IOException, ClientProtocolException { + private void testSearchReturnsResults(String search) throws IOException { int matches; HttpGet get = new HttpGet(ourServerBase + search); CloseableHttpResponse response = ourHttpClient.execute(get); @@ -2099,7 +2120,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testSearchWithInclude() throws Exception { + public void testSearchWithInclude() { Organization org = new Organization(); org.addIdentifier().setSystem("urn:system:rpdstu2").setValue("testSearchWithInclude01"); IdDt orgId = (IdDt) ourClient.create().resource(org).prettyPrint().encodedXml().execute().getId(); @@ -2127,7 +2148,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test(expected = InvalidRequestException.class) - public void testSearchWithInvalidSort() throws Exception { + public void testSearchWithInvalidSort() { Observation o = new Observation(); o.getCode().setText("testSearchWithInvalidSort"); myObservationDao.create(o, mySrd); @@ -2140,7 +2161,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testSearchWithMissing() throws Exception { + public void testSearchWithMissing() { ourLog.info("Starting testSearchWithMissing"); String methodName = "testSearchWithMissing"; @@ -2384,7 +2405,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { * Test for issue #60 */ @Test - public void testStoreUtf8Characters() throws Exception { + public void testStoreUtf8Characters() { Organization org = new Organization(); org.setName("測試醫院"); org.addIdentifier().setSystem("urn:system").setValue("testStoreUtf8Characters_01"); @@ -2438,7 +2459,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testUpdateInvalidUrl() throws IOException, Exception { + public void testUpdateInvalidUrl() throws Exception { String methodName = "testUpdateInvalidReference"; Patient pt = new Patient(); @@ -2460,7 +2481,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testUpdateRejectsInvalidTypes() throws InterruptedException { + public void testUpdateRejectsInvalidTypes() { Patient p1 = new Patient(); p1.addIdentifier().setSystem("urn:system").setValue("testUpdateRejectsInvalidTypes"); @@ -2565,7 +2586,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { } @Test - public void testUpdateResourceWithPrefer() throws IOException, Exception { + public void testUpdateResourceWithPrefer() throws Exception { String methodName = "testUpdateResourceWithPrefer"; Patient pt = new Patient(); @@ -2778,7 +2799,6 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test { Patient patient = new Patient(); patient.addName().addGiven("James" + StringUtils.leftPad("James", 1000000, 'A')); - ; patient.setBirthDate(new DateDt("2011-02-02")); Parameters input = new Parameters(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3BundleTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3BundleTest.java index a7280d1094f..1f6eca2bf6d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3BundleTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3BundleTest.java @@ -1,46 +1,68 @@ package ca.uhn.fhir.jpa.provider.dstu3; -import static org.junit.Assert.*; - +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; +import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle.BundleType; import org.hl7.fhir.dstu3.model.Composition; +import org.hl7.fhir.dstu3.model.MessageHeader; +import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.instance.model.api.IIdType; import org.junit.AfterClass; import org.junit.Test; -import ca.uhn.fhir.util.TestUtil; +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.*; public class ResourceProviderDstu3BundleTest extends BaseResourceProviderDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderDstu3BundleTest.class); + /** + * See #401 + */ + @Test + public void testBundlePreservesFullUrl() { + + Bundle bundle = new Bundle(); + bundle.setType(BundleType.DOCUMENT); + + Composition composition = new Composition(); + composition.setTitle("Visit Summary"); + bundle.addEntry().setFullUrl("http://foo").setResource(composition); + + IIdType id = ourClient.create().resource(bundle).execute().getId(); + + Bundle retBundle = ourClient.read().resource(Bundle.class).withId(id).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(retBundle)); + + assertEquals("http://foo", bundle.getEntry().get(0).getFullUrl()); + } + + @Test + public void testProcessMessage() { + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.MESSAGE); + + Parameters parameters = new Parameters(); + parameters.addParameter() + .setName("content") + .setResource(bundle); + try { + ourClient.operation().onType(MessageHeader.class).named(JpaConstants.OPERATION_PROCESS_MESSAGE).withParameters(parameters).execute(); + fail(); + } catch (NotImplementedOperationException e) { + assertThat(e.getMessage(), containsString("This operation is not yet implemented on this server")); + } + + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); } - /** - * See #401 - */ - @Test - public void testBundlePreservesFullUrl() throws Exception { - - Bundle bundle = new Bundle(); - bundle.setType(BundleType.DOCUMENT); - - Composition composition = new Composition(); - composition.setTitle("Visit Summary"); - bundle.addEntry().setFullUrl("http://foo").setResource(composition); - - IIdType id = ourClient.create().resource(bundle).execute().getId(); - - Bundle retBundle = ourClient.read().resource(Bundle.class).withId(id).execute(); - ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(retBundle)); - - assertEquals("http://foo", bundle.getEntry().get(0).getFullUrl()); - } - - } 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 b455ee5714d..c9df2834e90 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 @@ -3284,16 +3284,13 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { testSearchWithEmptyParameter("/Observation?code=bar&value-concept="); } - private void testSearchWithEmptyParameter(String url) throws IOException { - HttpGet get = new HttpGet(ourServerBase + url); - CloseableHttpResponse resp = ourHttpClient.execute(get); - try { + private void testSearchWithEmptyParameter(String theUrl) throws IOException { + HttpGet get = new HttpGet(ourServerBase + theUrl); + try (CloseableHttpResponse resp = ourHttpClient.execute(get)) { assertEquals(200, resp.getStatusLine().getStatusCode()); String respString = IOUtils.toString(resp.getEntity().getContent(), Constants.CHARSET_UTF8); Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, respString); assertEquals(1, bundle.getEntry().size()); - } finally { - IOUtils.closeQuietly(resp.getEntity().getContent()); } } 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 34daa680a1d..26ced1d0dfa 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 @@ -2,6 +2,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.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; @@ -10,8 +11,11 @@ import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 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.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; @@ -109,6 +113,16 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3 myLocalValueSetId = myValueSetDao.create(myLocalVs, mySrd).getId().toUnqualifiedVersionless(); } + private void createLocalVsWithIncludeConcept() { + myLocalVs = new ValueSet(); + myLocalVs.setUrl(URL_MY_VALUE_SET); + ConceptSetComponent include = myLocalVs.getCompose().addInclude(); + include.setSystem(URL_MY_CODE_SYSTEM); + include.addConcept().setCode("A"); + include.addConcept().setCode("AA"); + myLocalValueSetId = myValueSetDao.create(myLocalVs, mySrd).getId().toUnqualifiedVersionless(); + } + private void createLocalVsWithUnknownCode(CodeSystem codeSystem) { myLocalVs = new ValueSet(); myLocalVs.setUrl(URL_MY_VALUE_SET); @@ -172,7 +186,7 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3 * $expand?identifier=foo is legacy.. It's actually not valid in FHIR as of STU3 * but we supported it for longer than we should have so I don't want to delete * it right now. - * + *

    * https://groups.google.com/d/msgid/hapi-fhir/CAN2Cfy8kW%2BAOkgC6VjPsU3gRCpExCNZBmJdi-k5R_TWeyWH4tA%40mail.gmail.com?utm_medium=email&utm_source=footer */ @Test @@ -462,6 +476,29 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3 assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).booleanValue()); } + @Test + public void testValidateCodeOperationByCodeAndSystemInstanceOnInstance() throws IOException { + createLocalCsAndVs(); + createLocalVsWithIncludeConcept(); + + String url = ourServerBase + + "/ValueSet/" + myLocalValueSetId.getIdPart() + "/$validate-code?system=" + + UrlUtil.escapeUrlParam(FhirResourceDaoR4TerminologyTest.URL_MY_CODE_SYSTEM) + + "&code=AA"; + + ourLog.info("* Requesting: {}", url); + + HttpGet request = new HttpGet(url); + request.addHeader("Accept", "application/fhir+json"); + try (CloseableHttpResponse response = ourHttpClient.execute(request)) { + String respString = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(respString); + + Parameters respParam = myFhirCtx.newJsonParser().parseResource(Parameters.class, respString); + assertTrue(((BooleanType) respParam.getParameter().get(0).getValue()).booleanValue()); + } + } + @Test public void testValidateCodeOperationByCodeAndSystemType() { //@formatter:off diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/TerminologyUploaderProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/TerminologyUploaderProviderDstu3Test.java index 60b94ec3d2c..90bb75cc09f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/TerminologyUploaderProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/TerminologyUploaderProviderDstu3Test.java @@ -1,10 +1,15 @@ package ca.uhn.fhir.jpa.provider.dstu3; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.Matchers.greaterThan; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import ca.uhn.fhir.jpa.term.IHapiTerminologyLoaderSvc; +import ca.uhn.fhir.jpa.term.TerminologyLoaderSvcImpl; +import ca.uhn.fhir.jpa.term.ZipCollectionBuilder; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.util.TestUtil; +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.dstu3.model.*; +import org.junit.AfterClass; +import org.junit.Test; import java.io.ByteArrayOutputStream; import java.io.File; @@ -15,34 +20,45 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import org.apache.commons.io.IOUtils; -import org.hl7.fhir.dstu3.model.Attachment; -import org.hl7.fhir.dstu3.model.IntegerType; -import org.hl7.fhir.dstu3.model.Parameters; -import org.hl7.fhir.dstu3.model.StringType; -import org.hl7.fhir.dstu3.model.UriType; -import org.junit.AfterClass; -import org.junit.Test; - -import ca.uhn.fhir.jpa.term.IHapiTerminologyLoaderSvc; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.util.TestUtil; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.*; public class TerminologyUploaderProviderDstu3Test extends BaseResourceProviderDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TerminologyUploaderProviderDstu3Test.class); - private byte[] createLoincZip() throws IOException { + private static void addFile(ZipOutputStream theZos, String theFileName) throws IOException { + theZos.putNextEntry(new ZipEntry(theFileName)); + theZos.write(IOUtils.toByteArray(TerminologyUploaderProviderDstu3Test.class.getResourceAsStream("/loinc/" + theFileName))); + } + + public static byte[] createLoincZip() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(bos); - zos.putNextEntry(new ZipEntry("loinc.csv")); - zos.write(IOUtils.toByteArray(getClass().getResourceAsStream("/loinc/loinc.csv"))); - zos.putNextEntry(new ZipEntry("LOINC_2.54_MULTI-AXIAL_HIERARCHY.CSV")); - zos.write(IOUtils.toByteArray(getClass().getResourceAsStream("/loinc/LOINC_2.54_MULTI-AXIAL_HIERARCHY.CSV"))); + addFile(zos, "loincupload.properties"); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_PART_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_HIERARCHY_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_ANSWERLIST_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_ANSWERLIST_LINK_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_GROUP_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_GROUP_TERMS_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_PARENT_GROUP_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_PART_LINK_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_PART_RELATED_CODE_MAPPING_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_DOCUMENT_ONTOLOGY_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_RSNA_PLAYBOOK_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_CSV); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_IMAGING_DOCUMENT_CODES_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE); + addFile(zos, TerminologyLoaderSvcImpl.LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE); + zos.close(); + byte[] packageBytes = bos.toByteArray(); return packageBytes; } @@ -51,7 +67,7 @@ public class TerminologyUploaderProviderDstu3Test extends BaseResourceProviderDs ByteArrayOutputStream bos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(bos); - List inputNames = Arrays.asList("sct2_Concept_Full_INT_20160131.txt","sct2_Concept_Full-en_INT_20160131.txt","sct2_Description_Full-en_INT_20160131.txt","sct2_Identifier_Full_INT_20160131.txt","sct2_Relationship_Full_INT_20160131.txt","sct2_StatedRelationship_Full_INT_20160131.txt","sct2_TextDefinition_Full-en_INT_20160131.txt"); + List inputNames = Arrays.asList("sct2_Concept_Full_INT_20160131.txt", "sct2_Concept_Full-en_INT_20160131.txt", "sct2_Description_Full-en_INT_20160131.txt", "sct2_Identifier_Full_INT_20160131.txt", "sct2_Relationship_Full_INT_20160131.txt", "sct2_StatedRelationship_Full_INT_20160131.txt", "sct2_TextDefinition_Full-en_INT_20160131.txt"); for (String nextName : inputNames) { zos.putNextEntry(new ZipEntry("SnomedCT_Release_INT_20160131_Full/Terminology/" + nextName)); byte[] b = IOUtils.toByteArray(getClass().getResourceAsStream("/sct/" + nextName)); @@ -95,7 +111,7 @@ public class TerminologyUploaderProviderDstu3Test extends BaseResourceProviderDs String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); ourLog.info(resp); - assertThat(((IntegerType)respParam.getParameter().get(0).getValue()).getValue(), greaterThan(1)); + assertThat(((IntegerType) respParam.getParameter().get(0).getValue()).getValue(), greaterThan(1)); /* * Try uploading a second time @@ -150,7 +166,7 @@ public class TerminologyUploaderProviderDstu3Test extends BaseResourceProviderDs } @Test - public void testUploadPackageMissingUrl() throws Exception { + public void testUploadPackageMissingUrl() { try { ourClient .operation() @@ -179,7 +195,7 @@ public class TerminologyUploaderProviderDstu3Test extends BaseResourceProviderDs String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); ourLog.info(resp); - assertThat(((IntegerType)respParam.getParameter().get(0).getValue()).getValue(), greaterThan(1)); + assertThat(((IntegerType) respParam.getParameter().get(0).getValue()).getValue(), greaterThan(1)); } @Test @@ -205,7 +221,7 @@ public class TerminologyUploaderProviderDstu3Test extends BaseResourceProviderDs String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); ourLog.info(resp); - assertThat(((IntegerType)respParam.getParameter().get(0).getValue()).getValue(), greaterThan(1)); + assertThat(((IntegerType) respParam.getParameter().get(0).getValue()).getValue(), greaterThan(1)); } @AfterClass diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java index 62231f1bc5b..bf31760e130 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java @@ -40,64 +40,6 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource unregisterInterceptors(); } - /** - * See #778 - */ - @Test - public void testReadingObservationAccessRight() { - Practitioner practitioner1 = new Practitioner(); - final IIdType practitionerId1 = myClient.create().resource(practitioner1).execute().getId().toUnqualifiedVersionless(); - - Practitioner practitioner2 = new Practitioner(); - final IIdType practitionerId2 = myClient.create().resource(practitioner2).execute().getId().toUnqualifiedVersionless(); - - Patient patient = new Patient(); - patient.setActive(true); - final IIdType patientId = myClient.create().resource(patient).execute().getId().toUnqualifiedVersionless(); - - ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { - @Override - public List buildRuleList(RequestDetails theRequestDetails) { - // allow write all Observation resource - // allow read only Observation resource in which it has a practitioner1 or practitioner2 compartment - return new RuleBuilder().allow() - .write() - .resourcesOfType(Observation.class) - .withAnyId() - .andThen() - .allow() - .read() - .resourcesOfType(Observation.class) - .inCompartment("Practitioner", Arrays.asList(practitionerId1, practitionerId2)) - .andThen() - .denyAll() - .build(); - } - }); - - Observation obs1 = new Observation(); - obs1.setStatus(ObservationStatus.FINAL); - obs1.setPerformer( - Arrays.asList(new Reference(practitionerId1), new Reference(practitionerId2))); - IIdType oid1 = myClient.create().resource(obs1).execute().getId().toUnqualified(); - - // Observation with practitioner1 and practitioner1 as the Performer -> should have the read access - myClient.read().resource(Observation.class).withId(oid1).execute(); - - Observation obs2 = new Observation(); - obs2.setStatus(ObservationStatus.FINAL); - obs2.setSubject(new Reference(patientId)); - IIdType oid2 = myClient.create().resource(obs2).execute().getId().toUnqualified(); - - // Observation with patient as the subject -> read access should be blocked - try { - myClient.read().resource(Observation.class).withId(oid2).execute(); - fail(); - } catch (ForbiddenOperationException e) { - // good - } - } - /** * See #667 */ @@ -455,13 +397,11 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource }); - // Create a bundle that will be used as a transaction Bundle bundle = new Bundle(); bundle.setType(Bundle.BundleType.TRANSACTION); - String encounterId = "123-123"; String encounterSystem = "http://our.internal.code.system/encounter"; Encounter encounter = new Encounter(); @@ -523,8 +463,117 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource } + @Test + public void testPatchWithinCompartment() { + Patient pt1 = new Patient(); + pt1.setActive(true); + final IIdType pid1 = myClient.create().resource(pt1).execute().getId().toUnqualifiedVersionless(); + + Observation obs1 = new Observation(); + obs1.setStatus(ObservationStatus.FINAL); + obs1.setSubject(new Reference(pid1)); + IIdType oid1 = myClient.create().resource(obs1).execute().getId().toUnqualified(); + + Patient pt2 = new Patient(); + pt2.setActive(false); + final IIdType pid2 = myClient.create().resource(pt2).execute().getId().toUnqualifiedVersionless(); + + Observation obs2 = new Observation(); + obs2.setStatus(ObservationStatus.FINAL); + obs2.setSubject(new Reference(pid2)); + IIdType oid2 = myClient.create().resource(obs2).execute().getId().toUnqualified(); + + ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow().patch().allRequests().andThen() + .allow().write().allResources().inCompartment("Patient", pid1).andThen() + .allow().read().allResources().withAnyId().andThen() + .build(); + } + }); + + String patchBody = "[\n" + + " { \"op\": \"replace\", \"path\": \"Observation/status\", \"value\": \"amended\" }\n" + + " ]"; + + // Allowed + myClient.patch().withBody(patchBody).withId(oid1).execute(); + obs1 = myClient.read().resource(Observation.class).withId(oid1.toUnqualifiedVersionless()).execute(); + assertEquals(ObservationStatus.AMENDED, obs1.getStatus()); + + // Denied + try { + myClient.patch().withBody(patchBody).withId(oid2).execute(); + fail(); + } catch (ForbiddenOperationException e) { + // good + } + obs2 = myClient.read().resource(Observation.class).withId(oid2.toUnqualifiedVersionless()).execute(); + assertEquals(ObservationStatus.FINAL, obs2.getStatus()); + } + + /** + * See #778 + */ + @Test + public void testReadingObservationAccessRight() { + Practitioner practitioner1 = new Practitioner(); + final IIdType practitionerId1 = myClient.create().resource(practitioner1).execute().getId().toUnqualifiedVersionless(); + + Practitioner practitioner2 = new Practitioner(); + final IIdType practitionerId2 = myClient.create().resource(practitioner2).execute().getId().toUnqualifiedVersionless(); + + Patient patient = new Patient(); + patient.setActive(true); + final IIdType patientId = myClient.create().resource(patient).execute().getId().toUnqualifiedVersionless(); + + ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + // allow write all Observation resource + // allow read only Observation resource in which it has a practitioner1 or practitioner2 compartment + return new RuleBuilder().allow() + .write() + .resourcesOfType(Observation.class) + .withAnyId() + .andThen() + .allow() + .read() + .resourcesOfType(Observation.class) + .inCompartment("Practitioner", Arrays.asList(practitionerId1, practitionerId2)) + .andThen() + .denyAll() + .build(); + } + }); + + Observation obs1 = new Observation(); + obs1.setStatus(ObservationStatus.FINAL); + obs1.setPerformer( + Arrays.asList(new Reference(practitionerId1), new Reference(practitionerId2))); + IIdType oid1 = myClient.create().resource(obs1).execute().getId().toUnqualified(); + + // Observation with practitioner1 and practitioner1 as the Performer -> should have the read access + myClient.read().resource(Observation.class).withId(oid1).execute(); + + Observation obs2 = new Observation(); + obs2.setStatus(ObservationStatus.FINAL); + obs2.setSubject(new Reference(patientId)); + IIdType oid2 = myClient.create().resource(obs2).execute().getId().toUnqualified(); + + // Observation with patient as the subject -> read access should be blocked + try { + myClient.read().resource(Observation.class).withId(oid2).execute(); + fail(); + } catch (ForbiddenOperationException e) { + // good + } + } + private void unregisterInterceptors() { - for (IServerInterceptor next : new ArrayList(ourRestServer.getInterceptors())) { + for (IServerInterceptor next : new ArrayList<>(ourRestServer.getInterceptors())) { if (next instanceof AuthorizationInterceptor) { ourRestServer.unregisterInterceptor(next); } 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 84d872e667b..14659fca509 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,19 +2,27 @@ 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.util.ExpungeOptions; 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.IIdType; +import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.*; public class ExpungeR4Test extends BaseResourceProviderR4Test { @@ -58,11 +66,8 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { getDao(theId).read(theId); } - @Override - @Before - public void before() throws Exception { - super.before(); + public void createStandardPatients() { Patient p = new Patient(); p.setId("PT-ONEVERSION"); p.getMeta().addTag().setSystem("http://foo").setCode("bar"); @@ -105,7 +110,6 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { o.setStatus(Observation.ObservationStatus.FINAL); myDeletedObservationId = myObservationDao.create(o).getId(); myDeletedObservationId = myObservationDao.delete(myDeletedObservationId).getId(); - } private IFhirResourceDao getDao(IIdType theId) { @@ -126,6 +130,8 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { @Test public void testExpungeInstanceOldVersionsAndDeleted() { + createStandardPatients(); + Patient p = new Patient(); p.setId("PT-TWOVERSION"); p.getMeta().addTag().setSystem("http://foo").setCode("bar"); @@ -151,8 +157,35 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { assertGone(myDeletedObservationId); } + @Test + public void testExpungeAllVersionsDeletesRow() { + // Create then delete + Patient p = new Patient(); + p.setId("TEST"); + p.getMeta().addTag().setSystem("http://foo").setCode("bar"); + p.setActive(true); + p.addName().setFamily("FOO"); + 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()))); + + myPatientDao.expunge(new ExpungeOptions() + .setExpungeDeletedResources(true) + .setExpungeOldVersions(true)); + + runInTransaction(()-> assertThat(myResourceTableDao.findAll(), empty())); + runInTransaction(()-> assertThat(myResourceHistoryTableDao.findAll(), empty())); + runInTransaction(()-> assertThat(myForcedIdDao.findAll(), empty())); + + } + + @Test public void testExpungeInstanceVersionCurrentVersion() { + createStandardPatients(); try { myPatientDao.expunge(myTwoVersionPatientId.withVersion("2"), new ExpungeOptions() @@ -166,6 +199,8 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { @Test public void testExpungeInstanceVersionOldVersionsAndDeleted() { + createStandardPatients(); + Patient p = new Patient(); p.setId("PT-TWOVERSION"); p.getMeta().addTag().setSystem("http://foo").setCode("bar"); @@ -193,6 +228,8 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { @Test public void testExpungeSystemOldVersionsAndDeleted() { + createStandardPatients(); + mySystemDao.expunge(new ExpungeOptions() .setExpungeDeletedResources(true) .setExpungeOldVersions(true)); @@ -212,6 +249,8 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { @Test public void testExpungeTypeDeletedResources() { + createStandardPatients(); + myPatientDao.expunge(new ExpungeOptions() .setExpungeDeletedResources(true) .setExpungeOldVersions(false)); @@ -231,6 +270,8 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { @Test public void testExpungeTypeOldVersions() { + createStandardPatients(); + myPatientDao.expunge(new ExpungeOptions() .setExpungeDeletedResources(false) .setExpungeOldVersions(true)); @@ -251,6 +292,8 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { @Test public void testExpungeSystemEverything() { + createStandardPatients(); + mySystemDao.expunge(new ExpungeOptions() .setExpungeEverything(true)); @@ -270,6 +313,8 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test { @Test public void testExpungeTypeOldVersionsAndDeleted() { + createStandardPatients(); + myPatientDao.expunge(new ExpungeOptions() .setExpungeDeletedResources(true) .setExpungeOldVersions(true)); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4BundleTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4BundleTest.java index 6ea92ae282a..720ec0f66ea 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4BundleTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4BundleTest.java @@ -1,46 +1,68 @@ package ca.uhn.fhir.jpa.provider.r4; -import static org.junit.Assert.*; - +import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; +import ca.uhn.fhir.util.TestUtil; +import org.hl7.fhir.r4.model.MessageHeader; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleType; import org.hl7.fhir.r4.model.Composition; -import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Parameters; import org.junit.AfterClass; import org.junit.Test; -import ca.uhn.fhir.util.TestUtil; +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.*; public class ResourceProviderR4BundleTest extends BaseResourceProviderR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderR4BundleTest.class); + /** + * See #401 + */ + @Test + public void testBundlePreservesFullUrl() { + + Bundle bundle = new Bundle(); + bundle.setType(BundleType.DOCUMENT); + + Composition composition = new Composition(); + composition.setTitle("Visit Summary"); + bundle.addEntry().setFullUrl("http://foo").setResource(composition); + + IIdType id = myClient.create().resource(bundle).execute().getId(); + + Bundle retBundle = myClient.read().resource(Bundle.class).withId(id).execute(); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(retBundle)); + + assertEquals("http://foo", bundle.getEntry().get(0).getFullUrl()); + } + + @Test + public void testProcessMessage() { + + Bundle bundle = new Bundle(); + bundle.setType(BundleType.MESSAGE); + + Parameters parameters = new Parameters(); + parameters.addParameter() + .setName("content") + .setResource(bundle); + try { + myClient.operation().onType(MessageHeader.class).named(JpaConstants.OPERATION_PROCESS_MESSAGE).withParameters(parameters).execute(); + fail(); + } catch (NotImplementedOperationException e) { + assertThat(e.getMessage(), containsString("This operation is not yet implemented on this server")); + } + + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); } - /** - * See #401 - */ - @Test - public void testBundlePreservesFullUrl() throws Exception { - - Bundle bundle = new Bundle(); - bundle.setType(BundleType.DOCUMENT); - - Composition composition = new Composition(); - composition.setTitle("Visit Summary"); - bundle.addEntry().setFullUrl("http://foo").setResource(composition); - - IIdType id = myClient.create().resource(bundle).execute().getId(); - - Bundle retBundle = myClient.read().resource(Bundle.class).withId(id).execute(); - ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(retBundle)); - - assertEquals("http://foo", bundle.getEntry().get(0).getFullUrl()); - } - - } 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 f8c8d8c6f80..1b6bd81a6dd 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 @@ -3697,14 +3697,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { private void testSearchWithEmptyParameter(String url) throws IOException { HttpGet get = new HttpGet(ourServerBase + url); - CloseableHttpResponse resp = ourHttpClient.execute(get); - try { + try (CloseableHttpResponse resp = ourHttpClient.execute(get)) { assertEquals(200, resp.getStatusLine().getStatusCode()); String respString = IOUtils.toString(resp.getEntity().getContent(), Constants.CHARSET_UTF8); Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, respString); assertEquals(1, bundle.getEntry().size()); - } finally { - IOUtils.closeQuietly(resp.getEntity().getContent()); } } 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 df5af636f79..7f1171a64b2 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 @@ -10,8 +10,11 @@ import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 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.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; @@ -72,7 +75,6 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } private void createLocalCsAndVs() { - //@formatter:off CodeSystem codeSystem = new CodeSystem(); codeSystem.setUrl(URL_MY_CODE_SYSTEM); codeSystem.setContent(CodeSystemContentMode.COMPLETE); @@ -86,10 +88,17 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { .addConcept().setCode("B").setDisplay("Code B") .addConcept(new ConceptDefinitionComponent().setCode("BA").setDisplay("Code BA")) .addConcept(new ConceptDefinitionComponent().setCode("BB").setDisplay("Code BB")); - //@formatter:on myCodeSystemDao.create(codeSystem, mySrd); + } - createLocalVs(codeSystem); + private void createLocalVsWithIncludeConcept() { + myLocalVs = new ValueSet(); + myLocalVs.setUrl(URL_MY_VALUE_SET); + ConceptSetComponent include = myLocalVs.getCompose().addInclude(); + include.setSystem(URL_MY_CODE_SYSTEM); + include.addConcept().setCode("A"); + include.addConcept().setCode("AA"); + myLocalValueSetId = myValueSetDao.create(myLocalVs, mySrd).getId().toUnqualifiedVersionless(); } private void createLocalVs(CodeSystem codeSystem) { @@ -97,7 +106,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { myLocalVs.setUrl(URL_MY_VALUE_SET); ConceptSetComponent include = myLocalVs.getCompose().addInclude(); include.setSystem(codeSystem.getUrl()); - include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("childAA"); + include.addFilter().setProperty("concept").setOp(FilterOperator.ISA).setValue("ParentA"); myLocalValueSetId = myValueSetDao.create(myLocalVs, mySrd).getId().toUnqualifiedVersionless(); } @@ -119,7 +128,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } @Test - public void testExpandById() throws IOException { + public void testExpandById() { //@formatter:off Parameters respParam = myClient .operation() @@ -149,7 +158,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } @Test - public void testExpandByIdWithFilter() throws IOException { + public void testExpandByIdWithFilter() { //@formatter:off Parameters respParam = myClient @@ -208,7 +217,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { @Test - public void testExpandInlineVsAgainstBuiltInCs() throws IOException { + public void testExpandInlineVsAgainstBuiltInCs() { createLocalVsPointingAtBuiltInCodeSystem(); assertNotNull(myLocalValueSetId); @@ -229,7 +238,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } @Test - public void testExpandInlineVsAgainstExternalCs() throws IOException { + public void testExpandInlineVsAgainstExternalCs() { createExternalCsAndLocalVs(); assertNotNull(myLocalVs); myLocalVs.setId(""); @@ -304,7 +313,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } @Test - public void testExpandLocalVsAgainstBuiltInCs() throws IOException { + public void testExpandLocalVsAgainstBuiltInCs() { createLocalVsPointingAtBuiltInCodeSystem(); assertNotNull(myLocalValueSetId); @@ -325,7 +334,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } @Test - public void testExpandLocalVsAgainstExternalCs() throws IOException { + public void testExpandLocalVsAgainstExternalCs() { createExternalCsAndLocalVs(); assertNotNull(myLocalValueSetId); @@ -349,7 +358,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } @Test - public void testExpandLocalVsCanonicalAgainstExternalCs() throws IOException { + public void testExpandLocalVsCanonicalAgainstExternalCs() { createExternalCsAndLocalVs(); assertNotNull(myLocalValueSetId); @@ -373,7 +382,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { } @Test - public void testExpandLocalVsWithUnknownCode() throws IOException { + public void testExpandLocalVsWithUnknownCode() { createExternalCsAndLocalVsWithUnknownCode(); assertNotNull(myLocalValueSetId); @@ -400,8 +409,7 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { HttpPost post = new HttpPost(ourServerBase + "/ValueSet/%24expand"); post.setEntity(new StringEntity(string, ContentType.parse(ca.uhn.fhir.rest.api.Constants.CT_FHIR_JSON_NEW))); - CloseableHttpResponse resp = ourHttpClient.execute(post); - try { + try (CloseableHttpResponse resp = ourHttpClient.execute(post)) { String respString = IOUtils.toString(resp.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(respString); @@ -411,14 +419,11 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { assertEquals(400, resp.getStatusLine().getStatusCode()); assertThat(respString, containsString("Unknown FilterOperator code 'n'")); - } finally { - IOUtils.closeQuietly(resp); } } @Test public void testValidateCodeOperationByCodeAndSystemInstance() { - //@formatter:off Parameters respParam = myClient .operation() .onInstance(myExtensionalVsId) @@ -426,7 +431,6 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { .withParameter(Parameters.class, "code", new CodeType("8495-4")) .andParameter("system", new UriType("http://acme.org")) .execute(); - //@formatter:on String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); ourLog.info(resp); @@ -434,9 +438,51 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { assertEquals(true, ((BooleanType) respParam.getParameter().get(0).getValue()).booleanValue()); } + @Test + public void testValidateCodeOperationByCodeAndSystemInstanceOnType() throws IOException { + createLocalCsAndVs(); + + String url = ourServerBase + + "/ValueSet/$validate-code?system=" + + UrlUtil.escapeUrlParam(URL_MY_CODE_SYSTEM) + + "&code=AA"; + + HttpGet request = new HttpGet(url); + request.addHeader("Accept", "application/fhir+json"); + try (CloseableHttpResponse response = ourHttpClient.execute(request)) { + String respString = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(respString); + + Parameters respParam = myFhirCtx.newJsonParser().parseResource(Parameters.class, respString); + assertTrue(((BooleanType) respParam.getParameter().get(0).getValue()).booleanValue()); + } + } + + @Test + public void testValidateCodeOperationByCodeAndSystemInstanceOnInstance() throws IOException { + createLocalCsAndVs(); + createLocalVsWithIncludeConcept(); + + String url = ourServerBase + + "/ValueSet/" + myLocalValueSetId.getIdPart() + "/$validate-code?system=" + + UrlUtil.escapeUrlParam(URL_MY_CODE_SYSTEM) + + "&code=AA"; + + ourLog.info("* Requesting: {}", url); + + HttpGet request = new HttpGet(url); + request.addHeader("Accept", "application/fhir+json"); + try (CloseableHttpResponse response = ourHttpClient.execute(request)) { + String respString = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(respString); + + Parameters respParam = myFhirCtx.newJsonParser().parseResource(Parameters.class, respString); + assertTrue(((BooleanType) respParam.getParameter().get(0).getValue()).booleanValue()); + } + } + @Test public void testValidateCodeOperationByCodeAndSystemType() { - //@formatter:off Parameters respParam = myClient .operation() .onType(ValueSet.class) @@ -444,7 +490,6 @@ public class ResourceProviderR4ValueSetTest extends BaseResourceProviderR4Test { .withParameter(Parameters.class, "code", new CodeType("8450-9")) .andParameter("system", new UriType("http://acme.org")) .execute(); - //@formatter:on String resp = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); ourLog.info(resp); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/TerminologyUploaderProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/TerminologyUploaderProviderR4Test.java index b732bf86800..ddb99e5f82a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/TerminologyUploaderProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/TerminologyUploaderProviderR4Test.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.provider.r4; +import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3Test; import ca.uhn.fhir.jpa.term.IHapiTerminologyLoaderSvc; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.TestUtil; @@ -25,19 +26,6 @@ public class TerminologyUploaderProviderR4Test extends BaseResourceProviderR4Tes private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TerminologyUploaderProviderR4Test.class); - private byte[] createLoincZip() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ZipOutputStream zos = new ZipOutputStream(bos); - - zos.putNextEntry(new ZipEntry("loinc.csv")); - zos.write(IOUtils.toByteArray(getClass().getResourceAsStream("/loinc/loinc.csv"))); - zos.putNextEntry(new ZipEntry("LOINC_2.54_MULTI-AXIAL_HIERARCHY.CSV")); - zos.write(IOUtils.toByteArray(getClass().getResourceAsStream("/loinc/LOINC_2.54_MULTI-AXIAL_HIERARCHY.CSV"))); - zos.close(); - - byte[] packageBytes = bos.toByteArray(); - return packageBytes; - } private byte[] createSctZip() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -75,7 +63,7 @@ public class TerminologyUploaderProviderR4Test extends BaseResourceProviderR4Tes @Test public void testUploadLoinc() throws Exception { - byte[] packageBytes = createLoincZip(); + byte[] packageBytes = TerminologyUploaderProviderDstu3Test.createLoincZip(); //@formatter:off Parameters respParam = myClient 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 d87c0a84b47..90b3c320e07 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 @@ -321,18 +321,18 @@ public class SearchCoordinatorSvcImplTest { // ignore } - when(mySearchResultDao.findWithSearchUuid(any(Search.class), any(Pageable.class))).thenAnswer(new Answer>() { + when(mySearchResultDao.findWithSearchUuid(any(Search.class), any(Pageable.class))).thenAnswer(new Answer>() { @Override - public Page answer(InvocationOnMock theInvocation) throws Throwable { + public Page answer(InvocationOnMock theInvocation) throws Throwable { Pageable page = (Pageable) theInvocation.getArguments()[1]; - ArrayList results = new ArrayList(); + ArrayList results = new ArrayList(); int max = (page.getPageNumber() * page.getPageSize()) + page.getPageSize(); for (long i = page.getOffset(); i < max; i++) { - results.add(new SearchResult().setResourcePid(i + 10L)); + results.add(i + 10L); } - return new PageImpl(results); + return new PageImpl(results); } }); search.setStatus(SearchStatusEnum.FINISHED); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/IndexStressTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/IndexStressTest.java new file mode 100644 index 00000000000..0b963c30101 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/IndexStressTest.java @@ -0,0 +1,69 @@ +package ca.uhn.fhir.jpa.stresstest; + +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.util.StopWatch; +import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; +import org.hl7.fhir.dstu3.hapi.validation.CachingValidationSupport; +import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; +import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain; +import org.hl7.fhir.dstu3.model.Patient; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +public class IndexStressTest { + + private static final Logger ourLog = LoggerFactory.getLogger(IndexStressTest.class); + + @Test + public void testExtractSearchParams() { + Patient p = new Patient(); + p.addName().setFamily("FOO").addGiven("BAR").addGiven("BAR"); + 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); + extractor.start(); + + Map spMap = ctx + .getResourceDefinition("Patient") + .getSearchParams() + .stream() + .collect(Collectors.toMap(RuntimeSearchParam::getName, t -> t)); + when(searchParamRegistry.getActiveSearchParams(eq("Patient"))).thenReturn(spMap); + + ResourceTable entity = new ResourceTable(); + Set params = extractor.extractSearchParamStrings(entity, p); + + StopWatch sw = new StopWatch(); + int loops = 100; + for (int i = 0; i < loops; i++) { + entity = new ResourceTable(); + params = extractor.extractSearchParamStrings(entity, p); + } + + ourLog.info("Indexed {} times in {}ms/time", loops, sw.getMillisPerOperation(loops)); + + assertEquals(9, params.size()); + verify(mockValidationSupport, times(1)).fetchAllStructureDefinitions((any(FhirContext.class))); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/StressTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/StressTestDstu3Test.java index ca346af6e6a..29f8fe00a43 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/StressTestDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/StressTestDstu3Test.java @@ -48,7 +48,6 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { myRequestValidatingInterceptor.addValidatorModule(module); } - @Test public void testMultithreadedSearch() throws Exception { Bundle input = new Bundle(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcIntegrationDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcIntegrationDstu3Test.java index 6d2ab2a578b..ce8c0a5712b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcIntegrationDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcIntegrationDstu3Test.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.term; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem; +import ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Lists; @@ -52,22 +53,10 @@ public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test { .findFirst(); } - private Optional getPropertyPart(Parameters theParameters, String thePropName, String thePart) { - return theParameters - .getParameter() - .stream() - .filter(t -> t.getName().equals(thePropName)) - .flatMap(t -> t.getPart().stream()) - .filter(t -> t.getName().equals(thePart)) - .map(t -> (T) t.getValue()) - .findFirst(); - } - @Test public void testExpandWithPropertyCoding() throws Exception { ZipCollectionBuilder files = new ZipCollectionBuilder(); TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesToZip(files); - TerminologyLoaderSvcLoincTest.addLoincOptionalFilesToZip(files); myLoader.loadLoinc(files.getFiles(), mySrd); // Search by code @@ -121,7 +110,6 @@ public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test { public void testExpandWithPropertyString() throws Exception { ZipCollectionBuilder files = new ZipCollectionBuilder(); TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesToZip(files); - TerminologyLoaderSvcLoincTest.addLoincOptionalFilesToZip(files); myLoader.loadLoinc(files.getFiles(), mySrd); ValueSet input = new ValueSet(); @@ -144,7 +132,6 @@ public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test { public void testLookupWithProperties() throws Exception { ZipCollectionBuilder files = new ZipCollectionBuilder(); TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesToZip(files); - TerminologyLoaderSvcLoincTest.addLoincOptionalFilesToZip(files); myLoader.loadLoinc(files.getFiles(), mySrd); IFhirResourceDaoCodeSystem.LookupCodeResult result = myCodeSystemDao.lookupCode(new StringType("10013-1"), new StringType(IHapiTerminologyLoaderSvc.LOINC_URI), null, mySrd); @@ -172,11 +159,29 @@ public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test { } + @Test + public void testLookupWithProperties2() throws Exception { + ZipCollectionBuilder files = new ZipCollectionBuilder(); + TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesToZip(files); + myLoader.loadLoinc(files.getFiles(), mySrd); + + IFhirResourceDaoCodeSystem.LookupCodeResult result = myCodeSystemDao.lookupCode(new StringType("17788-1"), new StringType(IHapiTerminologyLoaderSvc.LOINC_URI), null, mySrd); + org.hl7.fhir.r4.model.Parameters parametersR4 = result.toParameters(null); + Parameters parameters = VersionConvertor_30_40.convertParameters(parametersR4); + + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(parameters)); + + Optional propertyValue = findProperty(parameters, "COMPONENT"); + assertTrue(propertyValue.isPresent()); + assertEquals(IHapiTerminologyLoaderSvc.LOINC_URI, propertyValue.get().getSystem()); + assertEquals("LP19258-0", propertyValue.get().getCode()); + assertEquals("Large unstained cells/100 leukocytes", propertyValue.get().getDisplay()); + } + @Test public void testLookupWithPropertiesExplicit() throws Exception { ZipCollectionBuilder files = new ZipCollectionBuilder(); TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesToZip(files); - TerminologyLoaderSvcLoincTest.addLoincOptionalFilesToZip(files); myLoader.loadLoinc(files.getFiles(), mySrd); IFhirResourceDaoCodeSystem.LookupCodeResult result = myCodeSystemDao.lookupCode(new StringType("10013-1"), new StringType(IHapiTerminologyLoaderSvc.LOINC_URI), null, mySrd); @@ -197,6 +202,30 @@ public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test { } + @Test + public void testValidateCodeFound() throws Exception { + ZipCollectionBuilder files = new ZipCollectionBuilder(); + TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesToZip(files); + myLoader.loadLoinc(files.getFiles(), mySrd); + + IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(null, null, new StringType("10013-1"), new StringType(IHapiTerminologyLoaderSvc.LOINC_URI), null, null, null, mySrd); + + assertTrue(result.isResult()); + assertEquals("Found code", result.getMessage()); + } + + @Test + public void testValidateCodeNotFound() throws Exception { + ZipCollectionBuilder files = new ZipCollectionBuilder(); + TerminologyLoaderSvcLoincTest.addLoincMandatoryFilesToZip(files); + myLoader.loadLoinc(files.getFiles(), mySrd); + + IFhirResourceDaoValueSet.ValidateCodeResult result = myValueSetDao.validateCode(null, null, new StringType("10013-1-9999999999"), new StringType(IHapiTerminologyLoaderSvc.LOINC_URI), null, null, null, mySrd); + + assertFalse(result.isResult()); + assertEquals("Code not found", result.getMessage()); + } + private Set toExpandedCodes(ValueSet theExpanded) { return theExpanded .getExpansion() diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincTest.java index d98f7a9f0ae..99080fa6619 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincTest.java @@ -13,12 +13,13 @@ import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.ValueSet; import org.junit.AfterClass; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.io.IOException; import java.util.HashMap; @@ -26,9 +27,10 @@ import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.*; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -92,7 +94,6 @@ public class TerminologyLoaderSvcLoincTest { @Test public void testLoadLoinc() throws Exception { addLoincMandatoryFilesToZip(myFiles); - addLoincOptionalFilesToZip(myFiles); // Actually do the load mySvc.loadLoinc(myFiles.getFiles(), details); @@ -116,6 +117,13 @@ public class TerminologyLoaderSvcLoincTest { assertEquals("EKG.MEAS", code.getStringProperty("CLASS")); assertEquals("R' wave amplitude in lead I", code.getDisplay()); + // Code with component that has a divisor + code = concepts.get("17788-1"); + assertEquals("17788-1", code.getCode()); + assertEquals(1, code.getCodingProperties("COMPONENT").size()); + assertEquals("http://loinc.org", code.getCodingProperties("COMPONENT").get(0).getSystem()); + assertEquals("LP19258-0", code.getCodingProperties("COMPONENT").get(0).getCode()); + // Loinc code with answer code = concepts.get("61438-8"); assertThat(code.getStringProperties("answer-list"), contains("LL1000-0")); @@ -221,9 +229,9 @@ public class TerminologyLoaderSvcLoincTest { assertEquals("NM", code.getCodingProperties(propertyName).get(0).getDisplay()); // RSNA Playbook - LOINC Part -> RadLex RID Mappings - conceptMap = conceptMaps.get(LoincRsnaPlaybookHandler.RID_MAPPING_CM_ID); - assertEquals(LoincRsnaPlaybookHandler.RID_MAPPING_CM_URI, conceptMap.getUrl()); - assertEquals(LoincRsnaPlaybookHandler.RID_MAPPING_CM_NAME, conceptMap.getName()); + conceptMap = conceptMaps.get(LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_ID); + assertEquals(LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_URI, conceptMap.getUrl()); + assertEquals(LoincPartRelatedCodeMappingHandler.LOINC_PART_TO_RID_PART_MAP_NAME, conceptMap.getName()); assertEquals(1, conceptMap.getGroup().size()); group = conceptMap.getGroupFirstRep(); // all entries have the same source and target so these should be null @@ -237,9 +245,9 @@ public class TerminologyLoaderSvcLoincTest { assertEquals(Enumerations.ConceptMapEquivalence.EQUAL, group.getElement().get(0).getTarget().get(0).getEquivalence()); // RSNA Playbook - LOINC Term -> RadLex RPID Mappings - conceptMap = conceptMaps.get(LoincRsnaPlaybookHandler.RPID_MAPPING_CM_ID); - assertEquals(LoincRsnaPlaybookHandler.RPID_MAPPING_CM_URI, conceptMap.getUrl()); - assertEquals(LoincRsnaPlaybookHandler.RPID_MAPPING_CM_NAME, conceptMap.getName()); + conceptMap = conceptMaps.get(LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_ID); + assertEquals(LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_URI, conceptMap.getUrl()); + assertEquals(LoincPartRelatedCodeMappingHandler.LOINC_TERM_TO_RPID_PART_MAP_NAME, conceptMap.getName()); assertEquals(1, conceptMap.getGroup().size()); group = conceptMap.getGroupFirstRep(); // all entries have the same source and target so these should be null @@ -286,7 +294,7 @@ public class TerminologyLoaderSvcLoincTest { // IEEE Medical Device Codes conceptMap = conceptMaps.get(LoincIeeeMedicalDeviceCodeHandler.LOINC_IEEE_CM_ID); - ourLog.info(FhirContext.forR4().newXmlParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); + ourLog.debug(FhirContext.forR4().newXmlParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); assertEquals(LoincIeeeMedicalDeviceCodeHandler.LOINC_IEEE_CM_NAME, conceptMap.getName()); assertEquals(LoincIeeeMedicalDeviceCodeHandler.LOINC_IEEE_CM_URI, conceptMap.getUrl()); assertEquals(1, conceptMap.getGroup().size()); @@ -308,9 +316,29 @@ public class TerminologyLoaderSvcLoincTest { assertEquals(9, vs.getCompose().getInclude().get(0).getConcept().size()); assertEquals("11525-3", vs.getCompose().getInclude().get(0).getConcept().get(0).getCode()); assertEquals("US Pelvis Fetus for pregnancy", vs.getCompose().getInclude().get(0).getConcept().get(0).getDisplay()); + + // Group - Parent + vs = valueSets.get("LG100-4"); + ourLog.info(FhirContext.forR4().newXmlParser().setPrettyPrint(true).encodeResourceToString(vs)); + assertEquals("Chem_DrugTox_Chal_Sero_Allergy", vs.getName()); + assertEquals("http://loinc.org/vs/LG100-4", vs.getUrl()); + assertEquals(1, vs.getCompose().getInclude().size()); + assertEquals(1, vs.getCompose().getInclude().get(0).getValueSet().size()); + assertEquals("http://loinc.org/vs/LG1695-8", vs.getCompose().getInclude().get(0).getValueSet().get(0).getValueAsString()); + + // Group - Child + vs = valueSets.get("LG1695-8"); + ourLog.info(FhirContext.forR4().newXmlParser().setPrettyPrint(true).encodeResourceToString(vs)); + assertEquals("1,4-Dichlorobenzene|MCnc|Pt|ANYBldSerPl", vs.getName()); + assertEquals("http://loinc.org/vs/LG1695-8", vs.getUrl()); + assertEquals(1, vs.getCompose().getInclude().size()); + assertEquals(2, vs.getCompose().getInclude().get(0).getConcept().size()); + assertEquals("17424-3", vs.getCompose().getInclude().get(0).getConcept().get(0).getCode()); + assertEquals("13006-2", vs.getCompose().getInclude().get(0).getConcept().get(1).getCode()); } @Test + @Ignore public void testLoadLoincMandatoryFilesOnly() throws IOException { addLoincMandatoryFilesToZip(myFiles); @@ -334,26 +362,29 @@ public class TerminologyLoaderSvcLoincTest { @Test public void testLoadLoincMissingMandatoryFiles() throws IOException { - addLoincOptionalFilesToZip(myFiles); + myFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_UPLOAD_PROPERTIES_FILE); + myFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_GROUP_FILE); // Actually do the load try { mySvc.loadLoinc(myFiles.getFiles(), details); fail(); } catch (UnprocessableEntityException e) { - assertEquals("Could not find the following mandatory files in input: [loinc.csv, MULTI-AXIAL_HIERARCHY.CSV]", e.getMessage()); + assertThat(e.getMessage(), containsString("Could not find the following mandatory files in input:")); + assertThat(e.getMessage(), containsString("Loinc.csv")); + assertThat(e.getMessage(), containsString("MultiAxialHierarchy.csv")); } } - static void addLoincMandatoryFilesToZip(ZipCollectionBuilder theFiles) throws IOException { - theFiles.addFileZip("/loinc/", "loinc.csv", TerminologyLoaderSvcImpl.LOINC_FILE); - theFiles.addFileZip("/loinc/", "hierarchy.csv", TerminologyLoaderSvcImpl.LOINC_HIERARCHY_FILE); - } - - static void addLoincOptionalFilesToZip(ZipCollectionBuilder theFiles) throws IOException { - theFiles.addFileZip("/loinc/", "loincupload.properties"); - theFiles.addFileZip("/loinc/", "AnswerList_Beta_1.csv", TerminologyLoaderSvcImpl.LOINC_ANSWERLIST_FILE); + public static void addLoincMandatoryFilesToZip(ZipCollectionBuilder theFiles) throws IOException { + theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_UPLOAD_PROPERTIES_FILE); + theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_GROUP_FILE); + theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_GROUP_TERMS_FILE); + theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_PARENT_GROUP_FILE); + theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_FILE, TerminologyLoaderSvcImpl.LOINC_FILE); + theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_HIERARCHY_FILE, TerminologyLoaderSvcImpl.LOINC_HIERARCHY_FILE); + theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_ANSWERLIST_FILE, TerminologyLoaderSvcImpl.LOINC_ANSWERLIST_FILE); theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_ANSWERLIST_LINK_FILE, TerminologyLoaderSvcImpl.LOINC_ANSWERLIST_LINK_FILE); theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_PART_FILE, TerminologyLoaderSvcImpl.LOINC_PART_FILE); theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_PART_LINK_FILE, TerminologyLoaderSvcImpl.LOINC_PART_LINK_FILE); @@ -363,13 +394,8 @@ public class TerminologyLoaderSvcLoincTest { theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_UNIVERSAL_LAB_ORDER_VALUESET_FILE); theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_IEEE_MEDICAL_DEVICE_CODE_MAPPING_TABLE_CSV); theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_IMAGING_DOCUMENT_CODES_FILE); - - /* - * Top 2000 files have versions in the filename so don't use the - * constant.. that way this is a better test - */ - theFiles.addFilePlain("/loinc/", "LOINC_1.6_Top2000CommonLabResultsSI.csv"); - theFiles.addFilePlain("/loinc/", "LOINC_1.6_Top2000CommonLabResultsUS.csv"); + theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_TOP2000_COMMON_LAB_RESULTS_SI_FILE); + theFiles.addFileZip("/loinc/", TerminologyLoaderSvcImpl.LOINC_TOP2000_COMMON_LAB_RESULTS_US_FILE); } @AfterClass 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 97fb67646c2..665eca92428 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 @@ -34,6 +34,12 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { @Autowired private ITermCodeSystemDao myTermCodeSystemDao; + @After + public void after() { + myDaoConfig.setDeferIndexingForCodesystemsOfSize(new DaoConfig().getDeferIndexingForCodesystemsOfSize()); + BaseHapiTerminologySvcImpl.setForceSaveDeferredAlwaysForUnitTest(false); + } + private IIdType createCodeSystem() { CodeSystem codeSystem = new CodeSystem(); codeSystem.setUrl(CS_URL); @@ -104,6 +110,31 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { return id; } + public void createLoincSystemWithSomeCodes() { + runInTransaction(() -> { + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setUrl(CS_URL); + codeSystem.setContent(CodeSystemContentMode.NOTPRESENT); + IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); + + ResourceTable table = myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalArgumentException::new); + + TermCodeSystemVersion cs = new TermCodeSystemVersion(); + cs.setResource(table); + + TermConcept code; + code = new TermConcept(cs, "50015-7"); + code.addPropertyString("SYSTEM", "Bld/Bone mar^Donor"); + cs.getConcepts().add(code); + + code = new TermConcept(cs, "43343-3"); + code.addPropertyString("SYSTEM", "Ser"); + cs.getConcepts().add(code); + + myTermSvc.storeNewCodeSystemVersion(table.getId(), CS_URL, "SYSTEM NAME", cs); + }); + } + @Test public void testCreateDuplicateCodeSystemUri() { CodeSystem codeSystem = new CodeSystem(); @@ -143,6 +174,40 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { } + @Test + public void testCreatePropertiesAndDesignationsWithDeferredConcepts() { + myDaoConfig.setDeferIndexingForCodesystemsOfSize(1); + BaseHapiTerminologySvcImpl.setForceSaveDeferredAlwaysForUnitTest(true); + + createCodeSystem(); + + Validate.notNull(myTermSvc); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + + ValueSet vs = new ValueSet(); + ValueSet.ConceptSetComponent include = vs.getCompose().addInclude(); + include.setSystem(CS_URL); + include.addConcept().setCode("childAAB"); + ValueSet outcome = myTermSvc.expandValueSet(vs); + + List codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, containsInAnyOrder("childAAB")); + + ValueSet.ValueSetExpansionContainsComponent concept = outcome.getExpansion().getContains().get(0); + assertEquals("childAAB", concept.getCode()); + assertEquals("http://example.com/my_code_system", concept.getSystem()); + assertEquals(null, concept.getDisplay()); + assertEquals("D1S", concept.getDesignation().get(0).getUse().getSystem()); + assertEquals("D1C", concept.getDesignation().get(0).getUse().getCode()); + assertEquals("D1D", concept.getDesignation().get(0).getUse().getDisplay()); + assertEquals("D1V", concept.getDesignation().get(0).getValue()); + } + @Test public void testExpandValueSetPropertySearch() { createCodeSystem(); @@ -194,6 +259,123 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { } + @Test + public void testExpandValueSetPropertySearchWithRegexExclude() { + createLoincSystemWithSomeCodes(); + + List codes; + ValueSet vs; + ValueSet outcome; + ValueSet.ConceptSetComponent exclude; + + // Include + vs = new ValueSet(); + vs.getCompose() + .addInclude() + .setSystem(CS_URL); + + exclude = vs.getCompose().addExclude(); + exclude.setSystem(CS_URL); + exclude + .addFilter() + .setProperty("SYSTEM") + .setOp(ValueSet.FilterOperator.REGEX) + .setValue(".*\\^Donor$"); + outcome = myTermSvc.expandValueSet(vs); + codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, containsInAnyOrder("43343-3")); + } + + @Test + public void testExpandValueSetPropertySearchWithRegexInclude() { + // create codes with "SYSTEM" property "Bld/Bone mar^Donor" and "Ser" + createLoincSystemWithSomeCodes(); + + List codes; + ValueSet vs; + ValueSet outcome; + ValueSet.ConceptSetComponent include; + + // Include + vs = new ValueSet(); + include = vs.getCompose().addInclude(); + include.setSystem(CS_URL); + include + .addFilter() + .setProperty("SYSTEM") + .setOp(ValueSet.FilterOperator.REGEX) + .setValue(".*\\^Donor$"); + outcome = myTermSvc.expandValueSet(vs); + codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, containsInAnyOrder("50015-7")); + + // Include + vs = new ValueSet(); + include = vs.getCompose().addInclude(); + include.setSystem(CS_URL); + include + .addFilter() + .setProperty("SYSTEM") + .setOp(ValueSet.FilterOperator.REGEX) + .setValue("\\^Donor$"); + outcome = myTermSvc.expandValueSet(vs); + codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, containsInAnyOrder("50015-7")); + + // Include + vs = new ValueSet(); + include = vs.getCompose().addInclude(); + include.setSystem(CS_URL); + include + .addFilter() + .setProperty("SYSTEM") + .setOp(ValueSet.FilterOperator.REGEX) + .setValue("\\^Dono$"); + outcome = myTermSvc.expandValueSet(vs); + codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, empty()); + + // Include + vs = new ValueSet(); + include = vs.getCompose().addInclude(); + include.setSystem(CS_URL); + include + .addFilter() + .setProperty("SYSTEM") + .setOp(ValueSet.FilterOperator.REGEX) + .setValue("^Donor$"); + outcome = myTermSvc.expandValueSet(vs); + codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, empty()); + + // Include + vs = new ValueSet(); + include = vs.getCompose().addInclude(); + include.setSystem(CS_URL); + include + .addFilter() + .setProperty("SYSTEM") + .setOp(ValueSet.FilterOperator.REGEX) + .setValue("\\^Dono"); + outcome = myTermSvc.expandValueSet(vs); + codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, containsInAnyOrder("50015-7")); + + // Include + vs = new ValueSet(); + include = vs.getCompose().addInclude(); + include.setSystem(CS_URL); + include + .addFilter() + .setProperty("SYSTEM") + .setOp(ValueSet.FilterOperator.REGEX) + .setValue("^Ser$"); + outcome = myTermSvc.expandValueSet(vs); + codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, containsInAnyOrder("43343-3")); + + } + @Test public void testExpandValueSetWholeSystem() { createCodeSystem(); @@ -209,73 +391,6 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { assertThat(codes, containsInAnyOrder("ParentWithNoChildrenA", "ParentWithNoChildrenB", "ParentWithNoChildrenC", "ParentA", "childAAA", "childAAB", "childAA", "childAB", "ParentB")); } - @Test - public void testPropertiesAndDesignationsPreservedInExpansion() { - createCodeSystem(); - - List codes; - - ValueSet vs = new ValueSet(); - ValueSet.ConceptSetComponent include = vs.getCompose().addInclude(); - include.setSystem(CS_URL); - include.addConcept().setCode("childAAB"); - ValueSet outcome = myTermSvc.expandValueSet(vs); - - codes = toCodesContains(outcome.getExpansion().getContains()); - assertThat(codes, containsInAnyOrder("childAAB")); - - ValueSet.ValueSetExpansionContainsComponent concept = outcome.getExpansion().getContains().get(0); - assertEquals("childAAB", concept.getCode()); - assertEquals("http://example.com/my_code_system", concept.getSystem()); - assertEquals(null, concept.getDisplay()); - assertEquals("D1S", concept.getDesignation().get(0).getUse().getSystem()); - assertEquals("D1C", concept.getDesignation().get(0).getUse().getCode()); - assertEquals("D1D", concept.getDesignation().get(0).getUse().getDisplay()); - assertEquals("D1V", concept.getDesignation().get(0).getValue()); - } - - @After - public void after() { - myDaoConfig.setDeferIndexingForCodesystemsOfSize(new DaoConfig().getDeferIndexingForCodesystemsOfSize()); - BaseHapiTerminologySvcImpl.setForceSaveDeferredAlwaysForUnitTest(false); - } - - - @Test - public void testCreatePropertiesAndDesignationsWithDeferredConcepts() { - myDaoConfig.setDeferIndexingForCodesystemsOfSize(1); - BaseHapiTerminologySvcImpl.setForceSaveDeferredAlwaysForUnitTest(true); - - createCodeSystem(); - - Validate.notNull(myTermSvc); - myTermSvc.saveDeferred(); - myTermSvc.saveDeferred(); - myTermSvc.saveDeferred(); - myTermSvc.saveDeferred(); - myTermSvc.saveDeferred(); - myTermSvc.saveDeferred(); - - ValueSet vs = new ValueSet(); - ValueSet.ConceptSetComponent include = vs.getCompose().addInclude(); - include.setSystem(CS_URL); - include.addConcept().setCode("childAAB"); - ValueSet outcome = myTermSvc.expandValueSet(vs); - - List codes = toCodesContains(outcome.getExpansion().getContains()); - assertThat(codes, containsInAnyOrder("childAAB")); - - ValueSet.ValueSetExpansionContainsComponent concept = outcome.getExpansion().getContains().get(0); - assertEquals("childAAB", concept.getCode()); - assertEquals("http://example.com/my_code_system", concept.getSystem()); - assertEquals(null, concept.getDisplay()); - assertEquals("D1S", concept.getDesignation().get(0).getUse().getSystem()); - assertEquals("D1C", concept.getDesignation().get(0).getUse().getCode()); - assertEquals("D1D", concept.getDesignation().get(0).getUse().getDisplay()); - assertEquals("D1V", concept.getDesignation().get(0).getValue()); - } - - @Test public void testFindCodesAbove() { IIdType id = createCodeSystem(); @@ -377,6 +492,31 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { assertThat(codes, empty()); } + @Test + public void testPropertiesAndDesignationsPreservedInExpansion() { + createCodeSystem(); + + List codes; + + ValueSet vs = new ValueSet(); + ValueSet.ConceptSetComponent include = vs.getCompose().addInclude(); + include.setSystem(CS_URL); + include.addConcept().setCode("childAAB"); + ValueSet outcome = myTermSvc.expandValueSet(vs); + + codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, containsInAnyOrder("childAAB")); + + ValueSet.ValueSetExpansionContainsComponent concept = outcome.getExpansion().getContains().get(0); + assertEquals("childAAB", concept.getCode()); + assertEquals("http://example.com/my_code_system", concept.getSystem()); + assertEquals(null, concept.getDisplay()); + assertEquals("D1S", concept.getDesignation().get(0).getUse().getSystem()); + assertEquals("D1C", concept.getDesignation().get(0).getUse().getCode()); + assertEquals("D1D", concept.getDesignation().get(0).getUse().getDisplay()); + assertEquals("D1V", concept.getDesignation().get(0).getValue()); + } + @Test public void testReindexTerminology() { IIdType id = createCodeSystem(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/util/HashFunctionTester.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/util/HashFunctionTester.java new file mode 100644 index 00000000000..2de91e905bf --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/util/HashFunctionTester.java @@ -0,0 +1,47 @@ +package ca.uhn.fhir.jpa.util; + +import ca.uhn.fhir.util.StopWatch; +import com.google.common.base.Charsets; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import com.helger.commons.base64.Base64; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class HashFunctionTester { + byte[] bytes1 = "ABCDiohseoiey oisegoyi loegsiyosg l".getBytes(Charsets.UTF_8); + byte[] bytes2 = "sepyo pyoiyi fdfff".getBytes(Charsets.UTF_8); + byte[] bytes3 = "us".getBytes(Charsets.UTF_8); + byte[] bytes4 = "f;pspus sgrygliy gfdygfio fdgxylxgfdyfgxloygfxdofgixyl yxxfly3ar3r8a3988".getBytes(Charsets.UTF_8); + + @Test + public void testHashBenchmark() { + + test(Hashing.murmur3_128(), "murmur3_128 "); + test(Hashing.sha256() , "sha256 "); + test(Hashing.sha384() , "sha384 "); + test(Hashing.sha512() , "sha512 "); + + } + + public void test(HashFunction theHashFunction, String theName) { + int loops = 10000; + StopWatch sw = new StopWatch(); + String output = ""; + for (int i = 0; i < loops; i++) { + Hasher hasher = theHashFunction.newHasher(); + hasher.putBytes(bytes1); + hasher.putBytes(bytes2); + hasher.putBytes(bytes3); + hasher.putBytes(bytes4); + output = Base64.encodeBytes(hasher.hash().asBytes()); + } + ourLog.info("{} took {}ms for {} or {}/second to generate {} chars: {}", theName, sw.getMillis(), loops, sw.getThroughput(loops, TimeUnit.SECONDS), output.length(), output); + } + + private static final Logger ourLog = LoggerFactory.getLogger(HashFunctionTester.class); +} diff --git a/hapi-fhir-jpaserver-base/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-base/src/test/resources/logback-test.xml index 91f8a74d3e2..b426d227835 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/logback-test.xml +++ b/hapi-fhir-jpaserver-base/src/test/resources/logback-test.xml @@ -1,5 +1,4 @@ - INFO diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/AnswerList_Beta_1.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/AnswerList.csv similarity index 100% rename from hapi-fhir-jpaserver-base/src/test/resources/loinc/AnswerList_Beta_1.csv rename to hapi-fhir-jpaserver-base/src/test/resources/loinc/AnswerList.csv diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/Group.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/Group.csv new file mode 100644 index 00000000000..66f5561ea1c --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/Group.csv @@ -0,0 +1,2 @@ +"ParentGroupId","GroupId","Group","Archetype","Status","VersionFirstReleased" +"LG100-4","LG1695-8","1,4-Dichlorobenzene|MCnc|Pt|ANYBldSerPl","","Active","" diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/GroupLoincTerms.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/GroupLoincTerms.csv new file mode 100644 index 00000000000..04c19759cfb --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/GroupLoincTerms.csv @@ -0,0 +1,3 @@ +"Category","GroupId","Archetype","LoincNumber","LongCommonName" +"Flowsheet","LG1695-8","","17424-3","1,4-Dichlorobenzene [Mass/volume] in Blood" +"Flowsheet","LG1695-8","","13006-2","1,4-Dichlorobenzene [Mass/volume] in Serum or Plasma" diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/Loinc.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/Loinc.csv new file mode 100644 index 00000000000..dfb7f1dd732 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/Loinc.csv @@ -0,0 +1,15 @@ +"LOINC_NUM","COMPONENT" ,"PROPERTY","TIME_ASPCT","SYSTEM" ,"SCALE_TYP","METHOD_TYP" ,"CLASS" ,"VersionLastChanged","CHNG_TYPE","DefinitionDescription" ,"STATUS","CONSUMER_NAME","CLASSTYPE","FORMULA","SPECIES","EXMPL_ANSWERS","SURVEY_QUEST_TEXT" ,"SURVEY_QUEST_SRC" ,"UNITSREQUIRED","SUBMITTED_UNITS","RELATEDNAMES2" ,"SHORTNAME" ,"ORDER_OBS" ,"CDISC_COMMON_TESTS","HL7_FIELD_SUBFIELD_ID","EXTERNAL_COPYRIGHT_NOTICE","EXAMPLE_UNITS","LONG_COMMON_NAME" ,"UnitsAndRange","DOCUMENT_SECTION","EXAMPLE_UCUM_UNITS","EXAMPLE_SI_UCUM_UNITS","STATUS_REASON","STATUS_TEXT","CHANGE_REASON_PUBLIC" ,"COMMON_TEST_RANK","COMMON_ORDER_RANK","COMMON_SI_TEST_RANK","HL7_ATTACHMENT_STRUCTURE","EXTERNAL_COPYRIGHT_LINK","PanelType","AskAtOrderEntry","AssociatedObservations" ,"VersionFirstReleased","ValidHL7AttachmentRequest" +"10013-1" ,"R' wave amplitude.lead I" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-I; R wave Amp L-I; Random; Right; Voltage" ,"R' wave Amp L-I" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead I" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , +"10014-9" ,"R' wave amplitude.lead II" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"2; Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-II; R wave Amp L-II; Random; Right; Voltage" ,"R' wave Amp L-II" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead II" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , +"10015-6" ,"R' wave amplitude.lead III" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"3; Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-III; R wave Amp L-III; Random; Right; Voltage" ,"R' wave Amp L-III" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead III" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , +"10016-4" ,"R' wave amplitude.lead V1" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V1; R wave Amp L-V1; Random; Right; Voltage" ,"R' wave Amp L-V1" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V1" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , +"1001-7" ,"DBG Ab" ,"Pr" ,"Pt" ,"Ser/Plas^donor" ,"Ord" , ,"BLDBK" ,"2.44" ,"MIN" , ,"ACTIVE", ,1 , , , , , , , ,"ABS; Aby; Antby; Anti; Antibodies; Antibody; Autoantibodies; Autoantibody; BLOOD BANK; Donna Bennett-Goodspeed; Donr; Ordinal; Pl; Plasma; Plsm; Point in time; QL; Qual; Qualitative; Random; Screen; SerP; SerPl; SerPl^donor; SerPlas; Serum; Serum or plasma; SR" ,"DBG Ab SerPl Donr Ql" ,"Observation", , , , ,"DBG Ab [Presence] in Serum or Plasma from donor" , , , , , , ,"The Property has been changed from ACnc to Pr (Presence) to reflect the new model for ordinal terms where results are based on presence or absence." ,0 ,0 ,0 , , , , , , , +"10017-2" ,"R' wave amplitude.lead V2" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V2; R wave Amp L-V2; Random; Right; Voltage" ,"R' wave Amp L-V2" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V2" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , +"10018-0" ,"R' wave amplitude.lead V3" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V3; R wave Amp L-V3; Random; Right; Voltage" ,"R' wave Amp L-V3" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V3" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , +"10019-8" ,"R' wave amplitude.lead V4" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V4; R wave Amp L-V4; Random; Right; Voltage" ,"R' wave Amp L-V4" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V4" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , +"10020-6" ,"R' wave amplitude.lead V5" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V5; R wave Amp L-V5; Random; Right; Voltage" ,"R' wave Amp L-V5" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V5" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , +"61438-8" ,"Each time you ate bread, toast or dinner rolls, how much did you usually eat in the past 30D","Find" ,"Pt" ,"^Patient" ,"Ord" ,"PhenX" ,"PHENX" ,"2.44" ,"MIN" , ,"TRIAL" , ,2 , , , ,"Each time you eat bread, toast or dinner rolls, how much do you usually eat?","PhenX.050201100100","N" , ,"Finding; Findings; How much bread in 30D; Last; Ordinal; Point in time; QL; Qual; Qualitative; Random; Screen" ,"How much bread in 30D PhenX", , , , , ,"Each time you ate bread, toast or dinner rolls, how much did you usually eat in the past 30 days [PhenX]", , , , , , , ,0 ,0 ,0 , , , , , , , +"10000-8" ,"R wave duration.lead AVR" ,"Time" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; Durat; ECG; EKG.MEASUREMENTS; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave dur L-AVR; R wave dur L-AVR; Random; Right" ,"R wave dur L-AVR" ,"Observation", , , ,"s" ,"R wave duration in lead AVR" , , ,"s" , , , , ,0 ,0 ,0 , , , , , , , +"17787-3" ,"Study report" ,"Find" ,"Pt" ,"Neck>Thyroid gland","Doc" ,"NM" ,"RAD" ,"2.61" ,"MIN" , ,"ACTIVE", ,2 , , , , , , , ,"Document; Finding; Findings; Imaging; Point in time; Radiology; Random; Study report; Thy" ,"NM Thyroid Study report" ,"Both" , , , , ,"NM Thyroid gland Study report" , , , , , , ,"Changed System from ""Thyroid"" for conformance with the LOINC/RadLex unified model.; Method of ""Radnuc"" was changed to ""NM"". The LOINC/RadLex Committee agreed to use a subset of the two-letter DICOM modality codes as the primary modality identifier." ,0 ,0 ,0 ,"IG exists" , , , ,"81220-6;72230-6" ,"1.0l" , +"17788-1" ,"Large unstained cells/100 leukocytes" ,"NFr" ,"Pt" ,"Bld" ,"Qn" ,"Automated count","HEM/BC" ,"2.50" ,"MIN" ,"Part of auto diff output of Bayer H*3S; peroxidase negative cells too large to be classified as lymph or basophil" ,"ACTIVE", ,1 , , , , , ,"Y" ,"%" ,"100WBC; Auto; Automated detection; Blood; Cell; Cellularity; Elec; Elect; Electr; HEMATOLOGY/CELL COUNTS; Leuc; Leuk; Leukocyte; Lkcs; LUC; Number Fraction; Percent; Point in time; QNT; Quan; Quant; Quantitative; Random; WB; WBC; WBCs; White blood cell; White blood cells; Whole blood" ,"LUC/leuk NFr Bld Auto" ,"Observation", , , ,"%" ,"Large unstained cells/100 leukocytes in Blood by Automated count" , , ,"%" , , , , ,1894 ,0 ,1894 , , , , , ,"1.0l" , +"11488-4" ,"Consultation note" ,"Find" ,"Pt" ,"{Setting}" ,"Doc" ,"{Role}" ,"DOC.ONTOLOGY","2.63" ,"MIN" , ,"ACTIVE", ,2 , , , , , , , ,"Consult note; DOC.ONT; Document; Encounter; Evaluation and management; Evaluation and management note; Finding; Findings; notes; Point in time; Random; Visit note" ,"Consult note" ,"Both" , , , , ,"Consult note" , , , , , , ,"Edit made because this term is conformant to the Document Ontology axis values and therefore are being placed in this class.; Based on Clinical LOINC Committee decision during the September 2014 meeting, {Provider} was changed to {Author Type} to emphasize a greater breadth of potential document authors. At the September 2015 Clinical LOINC Committee meeting, the Committee decided to change {Author Type} to {Role} to align with the 'Role' axis name in the LOINC Document Ontology.; Because it is too difficult to maintain and because the distinction between documents and sections is not clear-cut nor necessary in most cases, the DOCUMENT_SECTION field has been deemed to have little value. The field has been set to null in the December 2017 release in preparation for removal in the December 2018 release. These changes were approved by the Clinical LOINC Committee.",0 ,0 ,0 ,"IG exists" , , , ,"81222-2;72231-4;81243-8","1.0j-a" ,"Y" diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincAnswerListLink_Beta_1.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincAnswerListLink.csv similarity index 100% rename from hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincAnswerListLink_Beta_1.csv rename to hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincAnswerListLink.csv diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincPartLink_Beta_1.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincPartLink.csv similarity index 100% rename from hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincPartLink_Beta_1.csv rename to hapi-fhir-jpaserver-base/src/test/resources/loinc/LoincPartLink.csv diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/LOINC_2.54_MULTI-AXIAL_HIERARCHY.CSV b/hapi-fhir-jpaserver-base/src/test/resources/loinc/MultiAxialHierarchy.csv similarity index 100% rename from hapi-fhir-jpaserver-base/src/test/resources/loinc/LOINC_2.54_MULTI-AXIAL_HIERARCHY.CSV rename to hapi-fhir-jpaserver-base/src/test/resources/loinc/MultiAxialHierarchy.csv diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/ParentGroup.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/ParentGroup.csv new file mode 100644 index 00000000000..734a86c43dd --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/ParentGroup.csv @@ -0,0 +1,2 @@ +"ParentGroupId","ParentGroup","Status" +"LG100-4","Chem_DrugTox_Chal_Sero_Allergy","ACTIVE" diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/Part_Beta_1.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/Part.csv similarity index 89% rename from hapi-fhir-jpaserver-base/src/test/resources/loinc/Part_Beta_1.csv rename to hapi-fhir-jpaserver-base/src/test/resources/loinc/Part.csv index 8da9e598437..d5816ead286 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/loinc/Part_Beta_1.csv +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/Part.csv @@ -38,3 +38,9 @@ "LP7057-5","SYSTEM","Bld","Blood","ACTIVE" "LP6838-9","PROPERTY","NFr","Number Fraction","ACTIVE" "LP6141-8","METHOD","Automated count","Automated count","ACTIVE" +"LP15842-5","COMPONENT","Pyridoxine","Pyridoxine","ACTIVE" +"LP19258-0","COMPONENT","Large unstained cells","Large unstained cells","ACTIVE" +"LP32887-9","SYSTEM","{Setting}","{Setting}","ACTIVE" +"LP187178-1","METHOD","{Role}","Role-unspecified","ACTIVE" +"LP72311-1","COMPONENT","Consultation note","Consultation note","ACTIVE" + diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/PartRelatedCodeMapping_Beta_1.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/PartRelatedCodeMapping.csv similarity index 98% rename from hapi-fhir-jpaserver-base/src/test/resources/loinc/PartRelatedCodeMapping_Beta_1.csv rename to hapi-fhir-jpaserver-base/src/test/resources/loinc/PartRelatedCodeMapping.csv index fec510bd3f0..d0d7e122d12 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/loinc/PartRelatedCodeMapping_Beta_1.csv +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/PartRelatedCodeMapping.csv @@ -1,10 +1,12 @@ -"PartNumber","PartName","PartTypeName","ExtCodeId","ExtCodeDisplayName","ExtCodeSystem","MapType","ContentOrigin","ExtCodeSystemVersion","ExtCodeSystemCopyrightNotice" -"LP18172-4","Interferon.beta","COMPONENT"," 420710006","Interferon beta (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." -"LP31706-2","Nornicotine","COMPONENT","1018001","Nornicotine (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." -"LP15826-8","Prostaglandin F2","COMPONENT","10192006","Prostaglandin PGF2 (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." -"LP7400-7","Liver","SYSTEM","10200004","Liver structure (body structure)","http://snomed.info/sct","LOINC broader","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." -"LP29165-5","Liver.FNA","SYSTEM","10200004","Liver structure (body structure)","http://snomed.info/sct","LOINC broader","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." -"LP15666-8","Inosine","COMPONENT","102640000","Inosine (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." -"LP15943-1","Uronate","COMPONENT","102641001","Uronic acid (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." -"LP15791-4","Phenylketones","COMPONENT","102642008","Phenylketones (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." -"LP15721-1","Malonate","COMPONENT","102648007","Malonic acid (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"PartNumber","PartName","PartTypeName","ExtCodeId","ExtCodeDisplayName","ExtCodeSystem","MapType","ContentOrigin","ExtCodeSystemVersion","ExtCodeSystemCopyrightNotice" +"LP18172-4","Interferon.beta","COMPONENT"," 420710006","Interferon beta (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"LP31706-2","Nornicotine","COMPONENT","1018001","Nornicotine (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"LP15826-8","Prostaglandin F2","COMPONENT","10192006","Prostaglandin PGF2 (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"LP7400-7","Liver","SYSTEM","10200004","Liver structure (body structure)","http://snomed.info/sct","LOINC broader","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"LP29165-5","Liver.FNA","SYSTEM","10200004","Liver structure (body structure)","http://snomed.info/sct","LOINC broader","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"LP15666-8","Inosine","COMPONENT","102640000","Inosine (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"LP15943-1","Uronate","COMPONENT","102641001","Uronic acid (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"LP15791-4","Phenylketones","COMPONENT","102642008","Phenylketones (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"LP15721-1","Malonate","COMPONENT","102648007","Malonic acid (substance)","http://snomed.info/sct","Exact","Both","http://snomed.info/sct/900000000000207008/version/20170731","This material includes SNOMED Clinical Terms® (SNOMED CT®) which is used by permission of the International Health Terminology Standards Development Organisation (IHTSDO) under license. All rights reserved. SNOMED CT® was originally created by The College of American Pathologists. “SNOMED” and “SNOMED CT” are registered trademarks of the IHTSDO.This material includes content from the US Edition to SNOMED CT, which is developed and maintained by the U.S. National Library of Medicine and is available to authorized UMLS Metathesaurus Licensees from the UTS Downloads site at https://uts.nlm.nih.gov.Use of SNOMED CT content is subject to the terms and conditions set forth in the SNOMED CT Affiliate License Agreement. It is the responsibility of those implementing this product to ensure they are appropriately licensed and for more information on the license, including how to register as an Affiliate Licensee, please refer to http://www.snomed.org/snomed-ct/get-snomed-ct or info@snomed.org. This may incur a fee in SNOMED International non-Member countries." +"LP15842-5","Pyridoxine","COMPONENT","1054","Pyridoxine","http://pubchem.ncbi.nlm.nih.gov","Exact",,, +"LP15842-5","Pyridoxine","COMPONENT","1054","Pyridoxine","http://foo/bar","Exact",,, diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/LOINC_1.6_Top2000CommonLabResultsSI.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/Top2000CommonLabResultsSi.csv similarity index 100% rename from hapi-fhir-jpaserver-base/src/test/resources/loinc/LOINC_1.6_Top2000CommonLabResultsSI.csv rename to hapi-fhir-jpaserver-base/src/test/resources/loinc/Top2000CommonLabResultsSi.csv diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/LOINC_1.6_Top2000CommonLabResultsUS.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/Top2000CommonLabResultsUs.csv similarity index 100% rename from hapi-fhir-jpaserver-base/src/test/resources/loinc/LOINC_1.6_Top2000CommonLabResultsUS.csv rename to hapi-fhir-jpaserver-base/src/test/resources/loinc/Top2000CommonLabResultsUs.csv diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/hierarchy.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/hierarchy.csv deleted file mode 100644 index d7bd6d54884..00000000000 --- a/hapi-fhir-jpaserver-base/src/test/resources/loinc/hierarchy.csv +++ /dev/null @@ -1,10 +0,0 @@ -PATH_TO_ROOT,SEQUENCE,IMMEDIATE_PARENT,CODE,CODE_TEXT -,1,,LP31755-9,Microbiology -LP31755-9,1,LP31755-9,LP14559-6,Microorganism -LP31755-9.LP14559-6,1,LP14559-6,LP98185-9,Bacteria -LP31755-9.LP14559-6.LP98185-9,1,LP98185-9,LP14082-9,Bacteria -LP31755-9.LP14559-6.LP98185-9.LP14082-9,1,LP14082-9,LP52258-8,Bacteria | Body Fluid -LP31755-9.LP14559-6.LP98185-9.LP14082-9.LP52258-8,1,LP52258-8,41599-2,Bacteria Fld Ql Micro -LP31755-9.LP14559-6.LP98185-9.LP14082-9,2,LP14082-9,LP52260-4,Bacteria | Cerebral spinal fluid -LP31755-9.LP14559-6.LP98185-9.LP14082-9.LP52260-4,1,LP52260-4,41602-4,Bacteria CSF Ql Micro -LP31755-9.LP14559-6.LP98185-9.LP14082-9,3,LP14082-9,LP52960-9,Bacteria | Cervix diff --git a/hapi-fhir-jpaserver-base/src/test/resources/loinc/loinc.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/loinc.csv deleted file mode 100644 index 67e3ebfc0bd..00000000000 --- a/hapi-fhir-jpaserver-base/src/test/resources/loinc/loinc.csv +++ /dev/null @@ -1,16 +0,0 @@ -"LOINC_NUM","COMPONENT" ,"PROPERTY","TIME_ASPCT","SYSTEM" ,"SCALE_TYP","METHOD_TYP" ,"CLASS" ,"VersionLastChanged","CHNG_TYPE","DefinitionDescription" ,"STATUS","CONSUMER_NAME","CLASSTYPE","FORMULA","SPECIES","EXMPL_ANSWERS","SURVEY_QUEST_TEXT" ,"SURVEY_QUEST_SRC" ,"UNITSREQUIRED","SUBMITTED_UNITS","RELATEDNAMES2" ,"SHORTNAME" ,"ORDER_OBS" ,"CDISC_COMMON_TESTS","HL7_FIELD_SUBFIELD_ID","EXTERNAL_COPYRIGHT_NOTICE","EXAMPLE_UNITS","LONG_COMMON_NAME" ,"UnitsAndRange","DOCUMENT_SECTION","EXAMPLE_UCUM_UNITS","EXAMPLE_SI_UCUM_UNITS","STATUS_REASON","STATUS_TEXT","CHANGE_REASON_PUBLIC" ,"COMMON_TEST_RANK","COMMON_ORDER_RANK","COMMON_SI_TEST_RANK","HL7_ATTACHMENT_STRUCTURE","EXTERNAL_COPYRIGHT_LINK","PanelType","AskAtOrderEntry","AssociatedObservations" ,"VersionFirstReleased","ValidHL7AttachmentRequest" -"10013-1" ,"R' wave amplitude.lead I" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-I; R wave Amp L-I; Random; Right; Voltage" ,"R' wave Amp L-I" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead I" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , -"10014-9" ,"R' wave amplitude.lead II" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"2; Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-II; R wave Amp L-II; Random; Right; Voltage" ,"R' wave Amp L-II" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead II" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , -"10015-6" ,"R' wave amplitude.lead III" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"3; Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-III; R wave Amp L-III; Random; Right; Voltage" ,"R' wave Amp L-III" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead III" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , -"10016-4" ,"R' wave amplitude.lead V1" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V1; R wave Amp L-V1; Random; Right; Voltage" ,"R' wave Amp L-V1" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V1" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , -"1001-7" ,"DBG Ab" ,"Pr" ,"Pt" ,"Ser/Plas^donor" ,"Ord" , ,"BLDBK" ,"2.44" ,"MIN" , ,"ACTIVE", ,1 , , , , , , , ,"ABS; Aby; Antby; Anti; Antibodies; Antibody; Autoantibodies; Autoantibody; BLOOD BANK; Donna Bennett-Goodspeed; Donr; Ordinal; Pl; Plasma; Plsm; Point in time; QL; Qual; Qualitative; Random; Screen; SerP; SerPl; SerPl^donor; SerPlas; Serum; Serum or plasma; SR" ,"DBG Ab SerPl Donr Ql" ,"Observation", , , , ,"DBG Ab [Presence] in Serum or Plasma from donor" , , , , , , ,"The Property has been changed from ACnc to Pr (Presence) to reflect the new model for ordinal terms where results are based on presence or absence." ,0 ,0 ,0 , , , , , , , -"10017-2" ,"R' wave amplitude.lead V2" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V2; R wave Amp L-V2; Random; Right; Voltage" ,"R' wave Amp L-V2" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V2" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , -"10018-0" ,"R' wave amplitude.lead V3" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V3; R wave Amp L-V3; Random; Right; Voltage" ,"R' wave Amp L-V3" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V3" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , -"10019-8" ,"R' wave amplitude.lead V4" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V4; R wave Amp L-V4; Random; Right; Voltage" ,"R' wave Amp L-V4" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V4" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , -"10020-6" ,"R' wave amplitude.lead V5" ,"Elpot" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; ECG; EKG.MEASUREMENTS; Electrical potential; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave Amp L-V5; R wave Amp L-V5; Random; Right; Voltage" ,"R' wave Amp L-V5" ,"Observation", , , ,"mV" ,"R' wave amplitude in lead V5" , , ,"mV" , , , , ,0 ,0 ,0 , , , , , , , -"61438-8" ,"Each time you ate bread, toast or dinner rolls, how much did you usually eat in the past 30D","Find" ,"Pt" ,"^Patient" ,"Ord" ,"PhenX" ,"PHENX" ,"2.44" ,"MIN" , ,"TRIAL" , ,2 , , , ,"Each time you eat bread, toast or dinner rolls, how much do you usually eat?","PhenX.050201100100","N" , ,"Finding; Findings; How much bread in 30D; Last; Ordinal; Point in time; QL; Qual; Qualitative; Random; Screen" ,"How much bread in 30D PhenX", , , , , ,"Each time you ate bread, toast or dinner rolls, how much did you usually eat in the past 30 days [PhenX]", , , , , , , ,0 ,0 ,0 , , , , , , , -"10000-8" ,"R wave duration.lead AVR" ,"Time" ,"Pt" ,"Heart" ,"Qn" ,"EKG" ,"EKG.MEAS" ,"2.48" ,"MIN" , ,"ACTIVE", ,2 , , , , , ,"Y" , ,"Cardiac; Durat; ECG; EKG.MEASUREMENTS; Electrocardiogram; Electrocardiograph; Hrt; Painter's colic; PB; Plumbism; Point in time; QNT; Quan; Quant; Quantitative; R prime; R' wave dur L-AVR; R wave dur L-AVR; Random; Right" ,"R wave dur L-AVR" ,"Observation", , , ,"s" ,"R wave duration in lead AVR" , , ,"s" , , , , ,0 ,0 ,0 , , , , , , , -"17787-3" ,"Study report" ,"Find" ,"Pt" ,"Neck>Thyroid gland","Doc" ,"NM" ,"RAD" ,"2.61" ,"MIN" , ,"ACTIVE", ,2 , , , , , , , ,"Document; Finding; Findings; Imaging; Point in time; Radiology; Random; Study report; Thy" ,"NM Thyroid Study report" ,"Both" , , , , ,"NM Thyroid gland Study report" , , , , , , ,"Changed System from ""Thyroid"" for conformance with the LOINC/RadLex unified model.; Method of ""Radnuc"" was changed to ""NM"". The LOINC/RadLex Committee agreed to use a subset of the two-letter DICOM modality codes as the primary modality identifier." ,0 ,0 ,0 ,"IG exists" , , , ,"81220-6;72230-6" ,"1.0l" , -"17788-1" ,"Large unstained cells/100 leukocytes" ,"NFr" ,"Pt" ,"Bld" ,"Qn" ,"Automated count","HEM/BC" ,"2.50" ,"MIN" ,"Part of auto diff output of Bayer H*3S; peroxidase negative cells too large to be classified as lymph or basophil","ACTIVE", ,1 , , , , , ,"Y" ,"%" ,"100WBC; Auto; Automated detection; Blood; Cell; Cellularity; Elec; Elect; Electr; HEMATOLOGY/CELL COUNTS; Leuc; Leuk; Leukocyte; Lkcs; LUC; Number Fraction; Percent; Point in time; QNT; Quan; Quant; Quantitative; Random; WB; WBC; WBCs; White blood cell; White blood cells; Whole blood","LUC/leuk NFr Bld Auto" ,"Observation", , , ,"%" ,"Large unstained cells/100 leukocytes in Blood by Automated count" , , ,"%" , , , , ,1894 ,0 ,1894 , , , , , ,"1.0l" , -"11488-4" ,"Consultation note" ,"Find" ,"Pt" ,"{Setting}" ,"Doc" ,"{Role}" ,"DOC.ONTOLOGY","2.63" ,"MIN" , ,"ACTIVE", ,2 , , , , , , , ,"Consult note; DOC.ONT; Document; Encounter; Evaluation and management; Evaluation and management note; Finding; Findings; notes; Point in time; Random; Visit note" ,"Consult note" ,"Both" , , , , ,"Consult note" , , , , , , ,"Edit made because this term is conformant to the Document Ontology axis values and therefore are being placed in this class.; Based on Clinical LOINC Committee decision during the September 2014 meeting, {Provider} was changed to {Author Type} to emphasize a greater breadth of potential document authors. At the September 2015 Clinical LOINC Committee meeting, the Committee decided to change {Author Type} to {Role} to align with the 'Role' axis name in the LOINC Document Ontology.; Because it is too difficult to maintain and because the distinction between documents and sections is not clear-cut nor necessary in most cases, the DOCUMENT_SECTION field has been deemed to have little value. The field has been set to null in the December 2017 release in preparation for removal in the December 2018 release. These changes were approved by the Clinical LOINC Committee.",0 ,0 ,0 ,"IG exists" , , , ,"81222-2;72231-4;81243-8","1.0j-a" ,"Y" - diff --git a/hapi-fhir-jpaserver-elasticsearch/src/main/java/ca/uhn/fhir/jpa/search/ElasticsearchMappingProvider.java b/hapi-fhir-jpaserver-elasticsearch/src/main/java/ca/uhn/fhir/jpa/search/ElasticsearchMappingProvider.java index 0337b6f64b4..b52ef1230d3 100644 --- a/hapi-fhir-jpaserver-elasticsearch/src/main/java/ca/uhn/fhir/jpa/search/ElasticsearchMappingProvider.java +++ b/hapi-fhir-jpaserver-elasticsearch/src/main/java/ca/uhn/fhir/jpa/search/ElasticsearchMappingProvider.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.search; * #L% */ +import org.apache.lucene.analysis.core.WhitespaceTokenizerFactory; import org.hibernate.search.elasticsearch.analyzer.definition.ElasticsearchAnalysisDefinitionRegistryBuilder; import org.hibernate.search.elasticsearch.analyzer.definition.ElasticsearchAnalysisDefinitionProvider; @@ -57,5 +58,8 @@ public class ElasticsearchMappingProvider implements ElasticsearchAnalysisDefini builder.analyzer("exactAnalyzer").withTokenizer("standard"); builder.analyzer("conceptParentPidsAnalyzer").withTokenizer("whitespace"); + + builder.analyzer("termConceptPropertyAnalyzer").withTokenizer("whitespace"); + } } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt b/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt index b2f8445cc2e..b2e6da3861a 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt +++ b/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt @@ -136,3 +136,62 @@ 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 hfj_history_tag cascade; +drop table hfj_forced_id cascade; +drop table hfj_res_link cascade; +drop table hfj_spidx_coords cascade; +drop table hfj_spidx_date cascade; +drop table hfj_spidx_number cascade; +drop table hfj_spidx_quantity cascade; +drop table hfj_spidx_string cascade; +drop table hfj_spidx_token cascade; +drop table hfj_spidx_uri cascade; +drop table hfj_res_tag cascade; +drop table hfj_search_result cascade; +drop table hfj_search_include cascade; +drop table hfj_search cascade; +drop table hfj_res_param_present cascade; +drop table hfj_idx_cmp_string_uniq cascade; +drop table hfj_subscription_stats cascade; +drop table trm_concept_property cascade; +drop table trm_concept_pc_link cascade; +drop table trm_concept cascade; +drop table trm_codesystem_ver cascade; +drop table trm_codesystem cascade; +DROP TABLE hfj_resource cascade; +DROP TABLE hfj_res_ver cascade; +drop table hfj_search_parm cascade; +drop table hfj_tag_def cascade; + +drop index IDX_FORCEDID_TYPE_FORCEDID; +alter table hfj_forced_id drop constraint idx_forcedid_type_resid; + + + +Upgrading +drop index IDX_SP_STRING; +create index IDX_SP_STRING_HASH_NRM; +create index IDX_SP_STRING_HASH_EXCT; +drop index IDX_SP_TOKEN; +drop index IDX_SP_TOKEN_UNQUAL; +create index IDX_SP_TOKEN_HASH; +create index IDX_SP_TOKEN_HASH_S; +create index IDX_SP_TOKEN_HASH_SV; +create index IDX_SP_TOKEN_HASH_V; +drop index IDX_SP_DATE; +create index IDX_SP_DATE_HASH; +drop index IDX_SP_QUANTITY; +create index IDX_SP_QUANTITY_HASH; +create index IDX_SP_QUANTITY_HASH_UN; +drop index IDX_FORCEDID_TYPE_FORCEDID; +alter table hfj_forced_id drop constraint idx_forcedid_type_resid; +create index IDX_FORCEDID_TYPE_FID; +drop index IDX_SP_NUMBER; +create index IDX_SP_NUMBER_HASH_VAL; + +drop column SP_ID from table HFJ_RES_PARAM_PRESENT; +drop index IDX_SEARCHPARM_RESTYPE_SPNAME; +drop index IDX_RESPARMPRESENT_SPID_RESID; +drop table HFJ_SEARCH_PARM; 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 1dd093562fd..6c3e2be0607 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 @@ -7,6 +7,7 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.server.IRestfulServerDefaults; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; +import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -19,6 +20,8 @@ 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; @@ -184,6 +187,21 @@ public abstract class RequestDetails { public void setParameters(Map theParams) { myParameters = theParams; myUnqualifiedToQualifiedNames = null; + + // Sanitize keys if necessary to prevent injection attacks + boolean needsSanitization = false; + for (String nextKey : theParams.keySet()) { + if (UrlUtil.isNeedsSanitization(nextKey)) { + needsSanitization = true; + break; + } + } + if (needsSanitization) { + myParameters = myParameters + .entrySet() + .stream() + .collect(Collectors.toMap(t -> UrlUtil.sanitizeUrlPart((String) ((Map.Entry) t).getKey()), t -> (String[]) ((Map.Entry) t).getValue())); + } } /** 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 dfad1a42e97..f898adf2654 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -1246,7 +1246,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer buildRuleList(RequestDetails theRequestDetails) { - return new ArrayList(); + return new ArrayList<>(); } private OperationExamineDirection determineOperationDirection(RestOperationTypeEnum theOperation, IBaseResource theRequestResource) { @@ -407,7 +407,9 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter private final IAuthRule myDecidingRule; private final PolicyEnum myDecision; - public Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) { + Verdict(PolicyEnum theDecision, IAuthRule theDecidingRule) { + Validate.notNull(theDecision); + myDecision = theDecision; myDecidingRule = theDecidingRule; } 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 c378b2b04bb..bdb381ae088 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 @@ -35,6 +35,7 @@ abstract class BaseRule implements IAuthRule { private String myName; private PolicyEnum myMode; private List myTesters; + private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker; BaseRule(String theRuleName) { myName = theRuleName; @@ -51,7 +52,7 @@ abstract class BaseRule implements IAuthRule { public void addTesters(List theTesters) { theTesters.forEach(this::addTester); } - + boolean applyTesters(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IIdType theInputResourceId, IBaseResource theInputResource, IBaseResource theOutputResource) { boolean retVal = true; if (theOutputResource == null) { @@ -69,8 +70,9 @@ abstract class BaseRule implements IAuthRule { return myMode; } - void setMode(PolicyEnum theRuleMode) { + BaseRule setMode(PolicyEnum theRuleMode) { myMode = theRuleMode; + return this; } @Override @@ -78,6 +80,14 @@ abstract class BaseRule implements IAuthRule { return myName; } + public RuleBuilder.ITenantApplicabilityChecker getTenantApplicabilityChecker() { + return myTenantApplicabilityChecker; + } + + public final void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) { + myTenantApplicabilityChecker = theTenantApplicabilityChecker; + } + public List getTesters() { if (myTesters == null) { return Collections.emptyList(); @@ -85,6 +95,16 @@ abstract class BaseRule implements IAuthRule { return Collections.unmodifiableList(myTesters); } + public boolean isOtherTenant(RequestDetails theRequestDetails) { + boolean otherTenant = false; + if (getTenantApplicabilityChecker() != null) { + if (!getTenantApplicabilityChecker().applies(theRequestDetails)) { + otherTenant = true; + } + } + return otherTenant; + } + Verdict newVerdict() { return new Verdict(myMode, this); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderPatch.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderPatch.java new file mode 100644 index 00000000000..4893f319c7e --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderPatch.java @@ -0,0 +1,40 @@ +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% + */ + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public interface IAuthRuleBuilderPatch { + + /** + * With this setting, all patch requests will be permitted + * to proceed. This rule will not permit the + * {@link ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor#resourceCreated(RequestDetails, IBaseResource)} + * and + * {@link ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor#resourceUpdated(RequestDetails, IBaseResource, IBaseResource)} + * methods if your server supports {@link ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor}. + * In that case, additional rules are generally required in order to + * permit write operations. + */ + IAuthRuleFinished allRequests(); + +} 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 e604ebf4769..80e68a7725d 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 @@ -69,6 +69,11 @@ public interface IAuthRuleBuilderRule { */ IAuthRuleBuilderOperation operation(); + /** + * This rule applies to a FHIR patch operation + */ + IAuthRuleBuilderPatch patch(); + /** * This rule applies to any FHIR operation involving reading, including * read, vread, search, and 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 e9571bb555d..fcb0f1e8911 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 @@ -33,7 +33,6 @@ import java.util.Set; class OperationRule extends BaseRule implements IAuthRule { - private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker; private String myOperationName; private boolean myAppliesToServer; private HashSet> myAppliesToTypes; @@ -43,35 +42,35 @@ class OperationRule extends BaseRule implements IAuthRule { private boolean myAppliesToAnyInstance; private boolean myAppliesAtAnyLevel; - public OperationRule(String theRuleName) { + OperationRule(String theRuleName) { super(theRuleName); } - public void appliesAtAnyLevel(boolean theAppliesAtAnyLevel) { + void appliesAtAnyLevel(boolean theAppliesAtAnyLevel) { myAppliesAtAnyLevel = theAppliesAtAnyLevel; } - public void appliesToAnyInstance() { + void appliesToAnyInstance() { myAppliesToAnyInstance = true; } - public void appliesToAnyType() { + void appliesToAnyType() { myAppliesToAnyType = true; } - public void appliesToInstances(List theAppliesToIds) { + void appliesToInstances(List theAppliesToIds) { myAppliesToIds = theAppliesToIds; } - public void appliesToInstancesOfType(HashSet> theAppliesToTypes) { + void appliesToInstancesOfType(HashSet> theAppliesToTypes) { myAppliesToInstancesOfType = theAppliesToTypes; } - public void appliesToServer() { + void appliesToServer() { myAppliesToServer = true; } - public void appliesToTypes(HashSet> theAppliesToTypes) { + void appliesToTypes(HashSet> theAppliesToTypes) { myAppliesToTypes = theAppliesToTypes; } @@ -79,10 +78,8 @@ class OperationRule extends BaseRule implements IAuthRule { public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set theFlags) { FhirContext ctx = theRequestDetails.getServer().getFhirContext(); - if (myTenantApplicabilityChecker != null) { - if (!myTenantApplicabilityChecker.applies(theRequestDetails)) { - return null; - } + if (isOtherTenant(theRequestDetails)) { + return null; } boolean applies = false; @@ -203,8 +200,4 @@ class OperationRule extends BaseRule implements IAuthRule { myOperationName = theOperationName; } - public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) { - myTenantApplicabilityChecker = theTenantApplicabilityChecker; - } - } 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 81d523e1e0c..bc0ead08d80 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 @@ -33,7 +33,7 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; public class RuleBuilder implements IAuthRuleBuilder { - public static final String[] EMPTY_STRING_ARRAY = new String[0]; + private static final String[] EMPTY_STRING_ARRAY = new String[0]; private ArrayList myRules; public RuleBuilder() { @@ -95,19 +95,12 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderFinished implements IAuthRuleFinished, IAuthRuleBuilderRuleOpClassifierFinished, IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId { - private final RuleImplOp myOpRule; - private final OperationRule myOperationRule; - protected ITenantApplicabilityChecker myTenantApplicabilityChecker; + private final BaseRule myOpRule; + ITenantApplicabilityChecker myTenantApplicabilityChecker; private List myTesters; - RuleBuilderFinished(RuleImplOp theRule) { + RuleBuilderFinished(BaseRule theRule) { myOpRule = theRule; - myOperationRule = null; - } - - public RuleBuilderFinished(OperationRule theRule) { - myOpRule = null; - myOperationRule = theRule; } @Override @@ -136,16 +129,11 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId forTenantIds(final Collection theTenantIds) { - setTenantApplicabilityChecker(new ITenantApplicabilityChecker() { - @Override - public boolean applies(RequestDetails theRequest) { - return theTenantIds.contains(theRequest.getTenantId()); - } - }); + setTenantApplicabilityChecker(theRequest -> theTenantIds.contains(theRequest.getTenantId())); return this; } - public List getTesters() { + List getTesters() { if (myTesters == null) { return Collections.emptyList(); } @@ -159,23 +147,13 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId notForTenantIds(final Collection theTenantIds) { - setTenantApplicabilityChecker(new ITenantApplicabilityChecker() { - @Override - public boolean applies(RequestDetails theRequest) { - return !theTenantIds.contains(theRequest.getTenantId()); - } - }); + setTenantApplicabilityChecker(theRequest -> !theTenantIds.contains(theRequest.getTenantId())); return this; } private void setTenantApplicabilityChecker(ITenantApplicabilityChecker theTenantApplicabilityChecker) { myTenantApplicabilityChecker = theTenantApplicabilityChecker; - if (myOpRule != null) { myOpRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker); - } - if (myOperationRule != null) { - myOperationRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker); - } } @Override @@ -184,12 +162,7 @@ public class RuleBuilder implements IAuthRuleBuilder { myTesters = new ArrayList<>(); } myTesters.add(theTester); - if (myOperationRule != null) { - myOperationRule.addTester(theTester); - } - if (myOpRule != null) { - myOpRule.addTester(theTester); - } + myOpRule.addTester(theTester); return this; } @@ -236,6 +209,12 @@ public class RuleBuilder implements IAuthRuleBuilder { return new RuleBuilderRuleOperation(); } + @Override + public IAuthRuleBuilderPatch patch() { + myRuleOp = RuleOpEnum.PATCH; + return new PatchBuilder(); + } + @Override public IAuthRuleBuilderRuleOp read() { myRuleOp = RuleOpEnum.READ; @@ -286,8 +265,8 @@ public class RuleBuilder implements IAuthRuleBuilder { public class RuleBuilderRuleConditionalClassifier extends RuleBuilderFinished implements IAuthRuleBuilderRuleConditionalClassifier { - public RuleBuilderRuleConditionalClassifier() { - super((RuleImplOp) null); + RuleBuilderRuleConditionalClassifier() { + super(null); } @Override @@ -329,7 +308,7 @@ public class RuleBuilder implements IAuthRuleBuilder { Validate.notBlank(theId.getValue(), "theId.getValue() must not be null or empty"); Validate.notBlank(theId.getIdPart(), "theId must contain an ID part"); - return new RuleBuilderRuleOpClassifier(Arrays.asList(theId)).finished(); + return new RuleBuilderRuleOpClassifier(Collections.singletonList(theId)).finished(); } @Override @@ -435,7 +414,7 @@ public class RuleBuilder implements IAuthRuleBuilder { private String myOperationName; - public RuleBuilderRuleOperationNamed(String theOperationName) { + RuleBuilderRuleOperationNamed(String theOperationName) { if (theOperationName != null && !theOperationName.startsWith("$")) { myOperationName = '$' + theOperationName; } else { @@ -553,6 +532,17 @@ public class RuleBuilder implements IAuthRuleBuilder { } + private class PatchBuilder implements IAuthRuleBuilderPatch { + + @Override + public IAuthRuleFinished allRequests() { + BaseRule rule = new RuleImplPatch(myRuleName) + .setAllRequests(true) + .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/RuleImplConditional.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java index 7b9ac8140d6..67ed2b737c3 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java @@ -33,7 +33,6 @@ public class RuleImplConditional extends BaseRule implements IAuthRule { private AppliesTypeEnum myAppliesTo; private Set myAppliesToTypes; private RestOperationTypeEnum myOperationType; - private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker; RuleImplConditional(String theRuleName) { super(theRuleName); @@ -43,6 +42,10 @@ public class RuleImplConditional extends BaseRule implements IAuthRule { public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set theFlags) { + if (isOtherTenant(theRequestDetails)) { + return null; + } + if (theInputResourceId != null) { return null; } @@ -63,8 +66,8 @@ public class RuleImplConditional extends BaseRule implements IAuthRule { return null; } - if (myTenantApplicabilityChecker != null) { - if (!myTenantApplicabilityChecker.applies(theRequestDetails)) { + if (getTenantApplicabilityChecker() != null) { + if (!getTenantApplicabilityChecker().applies(theRequestDetails)) { return null; } } @@ -91,8 +94,4 @@ public class RuleImplConditional extends BaseRule implements IAuthRule { myOperationType = theOperationType; } - public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) { - myTenantApplicabilityChecker = theTenantApplicabilityChecker; - } - } 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 327f5d10584..2fd32eefcd4 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 @@ -14,10 +14,12 @@ import ca.uhn.fhir.util.FhirTerser; import org.apache.commons.codec.binary.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import org.hl7.fhir.instance.model.api.IAnyResource; 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 javax.annotation.Nullable; import java.util.Collection; import java.util.List; import java.util.Map; @@ -34,9 +36,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. @@ -55,7 +57,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { private RuleOpEnum myOp; private TransactionAppliesToEnum myTransactionAppliesToOp; private List myAppliesToInstances; - private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker; /** * Constructor @@ -68,10 +69,8 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set theFlags) { - if (myTenantApplicabilityChecker != null) { - if (!myTenantApplicabilityChecker.applies(theRequestDetails)) { - return null; - } + if (isOtherTenant(theRequestDetails)) { + return null; } FhirContext ctx = theRequestDetails.getServer().getFhirContext(); @@ -159,7 +158,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { case DELETE_TAGS: case META_ADD: case META_DELETE: - case PATCH: appliesToResource = theInputResource; appliesToResourceId = theInputResourceId; break; @@ -305,14 +303,12 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } } } -// if (myClassifierType == ClassifierTypeEnum.ANY_ID) { - if (appliesToResourceId != null && appliesToResourceId.hasResourceType()) { - Class type = theRequestDetails.getServer().getFhirContext().getResourceDefinition(appliesToResourceId.getResourceType()).getImplementingClass(); - if (myAppliesToTypes.contains(type) == false) { - return null; - } + if (appliesToResourceId != null && appliesToResourceId.hasResourceType()) { + Class type = theRequestDetails.getServer().getFhirContext().getResourceDefinition(appliesToResourceId.getResourceType()).getImplementingClass(); + if (myAppliesToTypes.contains(type) == false) { + return null; } -// } + } if (appliesToResourceType != null) { Class type = theRequestDetails.getServer().getFhirContext().getResourceDefinition(appliesToResourceType).getImplementingClass(); if (myAppliesToTypes.contains(type)) { @@ -351,6 +347,19 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } } + /* + * If the client has permission to read compartment + * Patient/ABC, then a search for Patient?_id=Patient/ABC + * should be permitted. This is kind of a one-off case, but + * it makes sense. + */ + if (next.getResourceType().equals(appliesToResourceType)) { + Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(appliesToSearchParams, next, IAnyResource.SP_RES_ID); + if (verdict != null) { + return verdict; + } + } + /* * If we're trying to read a resource that could potentially be * in the given compartment, we'll let the request through and @@ -377,13 +386,10 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { */ if (appliesToSearchParams != null && !theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) { for (RuntimeSearchParam nextRuntimeSearchParam : params) { - String[] values = appliesToSearchParams.get(nextRuntimeSearchParam.getName()); - if (values != null) { - for (String nextParameterValue : values) { - if (nextParameterValue.equals(next.getValue())) { - return new Verdict(PolicyEnum.ALLOW, this); - } - } + String name = nextRuntimeSearchParam.getName(); + Verdict verdict = checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(appliesToSearchParams, next, name); + if (verdict != null) { + return verdict; } } } else { @@ -409,6 +415,26 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { return newVerdict(); } + private Verdict checkForSearchParameterMatchingCompartmentAndReturnSuccessfulVerdictOrNull(Map theSearchParams, IIdType theCompartmentOwner, String theSearchParamName) { + Verdict verdict = null; + if (theSearchParams != null) { + String[] values = theSearchParams.get(theSearchParamName); + if (values != null) { + for (String nextParameterValue : values) { + if (nextParameterValue.equals(theCompartmentOwner.getValue())) { + verdict = new Verdict(PolicyEnum.ALLOW, this); + break; + } + if (nextParameterValue.equals(theCompartmentOwner.getIdPart())) { + verdict = new Verdict(PolicyEnum.ALLOW, this); + break; + } + } + } + } + return verdict; + } + public TransactionAppliesToEnum getTransactionAppliesToOp() { return myTransactionAppliesToOp; } @@ -424,6 +450,8 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { IBaseBundle request = (IBaseBundle) theInputResource; String bundleType = BundleUtil.getBundleType(theContext, request); + + //noinspection EnumSwitchStatementWhichMissesCases switch (theOp) { case TRANSACTION: return "transaction".equals(bundleType); @@ -463,9 +491,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { return this; } - public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) { - myTenantApplicabilityChecker = theTenantApplicabilityChecker; - } @Override public String toString() { @@ -474,7 +499,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { builder.append("transactionAppliesToOp", myTransactionAppliesToOp); builder.append("appliesTo", myAppliesTo); builder.append("appliesToTypes", myAppliesToTypes); - builder.append("appliesToTenant", myTenantApplicabilityChecker); + builder.append("appliesToTenant", getTenantApplicabilityChecker()); builder.append("classifierCompartmentName", myClassifierCompartmentName); builder.append("classifierCompartmentOwners", myClassifierCompartmentOwners); builder.append("classifierType", myClassifierType); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplPatch.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplPatch.java new file mode 100644 index 00000000000..d3f4235b6ae --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplPatch.java @@ -0,0 +1,58 @@ +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% + */ + +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +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 java.util.Set; + +class RuleImplPatch extends BaseRule { + private boolean myAllRequests; + + RuleImplPatch(String theRuleName) { + super(theRuleName); + } + + @Override + public AuthorizationInterceptor.Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set theFlags) { + if (isOtherTenant(theRequestDetails)) { + return null; + } + + if (myAllRequests) { + if (theOperation == RestOperationTypeEnum.PATCH) { + if (theInputResource == null && theOutputResource == null) { + return newVerdict(); + } + } + } + + return null; + } + + RuleImplPatch setAllRequests(boolean theAllRequests) { + myAllRequests = theAllRequests; + return this; + } +} 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 63bce75f93e..6ec6a79a8d6 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 @@ -29,5 +29,6 @@ enum RuleOpEnum { METADATA, BATCH, DELETE, - OPERATION + OPERATION, + PATCH } 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 5deaa1209e9..a359bede97e 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 @@ -122,7 +122,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; } - myReturnParams = new ArrayList(); + myReturnParams = new ArrayList<>(); if (theReturnParams != null) { for (OperationParam next : theReturnParams) { ReturnType type = new ReturnType(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/tenant/UrlBaseTenantIdentificationStrategy.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/tenant/UrlBaseTenantIdentificationStrategy.java index e444bc5d9ac..7887cbd9b8d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/tenant/UrlBaseTenantIdentificationStrategy.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/tenant/UrlBaseTenantIdentificationStrategy.java @@ -43,7 +43,7 @@ public class UrlBaseTenantIdentificationStrategy implements ITenantIdentificatio public void extractTenant(UrlPathTokenizer theUrlPathTokenizer, RequestDetails theRequestDetails) { String tenantId = null; if (theUrlPathTokenizer.hasMoreTokens()) { - tenantId = defaultIfBlank(theUrlPathTokenizer.nextToken(), null); + tenantId = defaultIfBlank(theUrlPathTokenizer.nextTokenUnescapedAndSanitized(), null); ourLog.trace("Found tenant ID {} in request string", tenantId); theRequestDetails.setTenantId(tenantId); } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/BinaryDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/BinaryDstu2Test.java index c80a47ed911..a68e9cc2896 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/BinaryDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/BinaryDstu2Test.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; @@ -13,6 +14,8 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.*; @@ -56,6 +59,34 @@ public class BinaryDstu2Test { TestUtil.clearAllStaticFieldsForUnitTest(); } + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + ResourceProvider binaryProvider = new ResourceProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + servlet.setResourceProviders(binaryProvider); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + + int timeout = 5; + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(timeout * 1000) + .setConnectionRequestTimeout(timeout * 1000) + .setSocketTimeout(timeout * 1000).build(); + + builder.setConnectionManager(connectionManager); + ourClient = builder.setDefaultRequestConfig(config).build(); + + } @Before public void before() { @@ -65,53 +96,56 @@ public class BinaryDstu2Test { @Test public void testReadWithExplicitTypeXml() throws Exception { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo?_format=xml"); - HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent(), "UTF-8"); - IOUtils.closeQuietly(status.getEntity().getContent()); + try (CloseableHttpResponse response = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(response.getEntity().getContent(), "UTF-8"); + IOUtils.closeQuietly(response.getEntity().getContent()); - ourLog.info(responseContent); - - assertEquals(200, status.getStatusLine().getStatusCode()); - assertThat(status.getFirstHeader("content-type").getValue(), startsWith(Constants.CT_FHIR_XML + ";")); - - Binary bin = ourCtx.newXmlParser().parseResource(Binary.class, responseContent); - assertEquals("foo", bin.getContentType()); - assertArrayEquals(new byte[] { 1, 2, 3, 4 }, bin.getContent()); + ourLog.info(responseContent); + + assertEquals(200, response.getStatusLine().getStatusCode()); + assertThat(response.getFirstHeader("content-type").getValue(), startsWith(Constants.CT_FHIR_XML + ";")); + + Binary bin = ourCtx.newXmlParser().parseResource(Binary.class, responseContent); + assertEquals("foo", bin.getContentType()); + assertArrayEquals(new byte[] { 1, 2, 3, 4 }, bin.getContent()); + } } @Test public void testReadWithExplicitTypeJson() throws Exception { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo?_format=json"); - HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent(), "UTF-8"); - IOUtils.closeQuietly(status.getEntity().getContent()); + try (CloseableHttpResponse response = ourClient.execute(httpGet)){ + String responseContent = IOUtils.toString(response.getEntity().getContent(), "UTF-8"); + IOUtils.closeQuietly(response.getEntity().getContent()); - ourLog.info(responseContent); - - assertEquals(200, status.getStatusLine().getStatusCode()); - assertThat(status.getFirstHeader("content-type").getValue(), startsWith(Constants.CT_FHIR_JSON + ";")); - - Binary bin = ourCtx.newJsonParser().parseResource(Binary.class, responseContent); - assertEquals("foo", bin.getContentType()); - assertArrayEquals(new byte[] { 1, 2, 3, 4 }, bin.getContent()); + ourLog.info(responseContent); + + assertEquals(200, response.getStatusLine().getStatusCode()); + assertThat(response.getFirstHeader("content-type").getValue(), startsWith(Constants.CT_FHIR_JSON + ";")); + + Binary bin = ourCtx.newJsonParser().parseResource(Binary.class, responseContent); + assertEquals("foo", bin.getContentType()); + assertArrayEquals(new byte[] { 1, 2, 3, 4 }, bin.getContent()); + } } - + // posts Binary directly @Test - public void testCreate() throws Exception { + public void testPostBinary() throws Exception { HttpPost http = new HttpPost("http://localhost:" + ourPort + "/Binary"); http.setEntity(new ByteArrayEntity(new byte[] { 1, 2, 3, 4 }, ContentType.create("foo/bar", "UTF-8"))); - HttpResponse status = ourClient.execute(http); - assertEquals(201, status.getStatusLine().getStatusCode()); - - assertEquals("foo/bar; charset=UTF-8", ourLast.getContentType()); - assertArrayEquals(new byte[] { 1, 2, 3, 4 }, ourLast.getContent()); + try (CloseableHttpResponse response = ourClient.execute(http)){ + assertEquals(201, response.getStatusLine().getStatusCode()); + assertEquals("foo/bar; charset=UTF-8", ourLast.getContentType()); + assertArrayEquals(new byte[] { 1, 2, 3, 4 }, ourLast.getContent()); + } } + // posts Binary as FHIR Resource @Test - public void testCreateWrongType() throws Exception { + public void testPostFhirBinary() throws Exception { Binary res = new Binary(); res.setContent(new byte[] { 1, 2, 3, 4 }); res.setContentType("text/plain"); @@ -120,114 +154,92 @@ public class BinaryDstu2Test { HttpPost http = new HttpPost("http://localhost:" + ourPort + "/Binary"); http.setEntity(new StringEntity(stringContent, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8"))); - HttpResponse status = ourClient.execute(http); - assertEquals(201, status.getStatusLine().getStatusCode()); - - assertEquals("application/json+fhir;charset=utf-8", ourLast.getContentType().replace(" ","").toLowerCase()); + try (CloseableHttpResponse response = ourClient.execute(http)) { + assertEquals(201, response.getStatusLine().getStatusCode()); + assertEquals("text/plain", ourLast.getContentType().replace(" ", "").toLowerCase()); + } } @Test public void testBinaryReadAcceptMissing() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary/foo"); - - HttpResponse status = ourClient.execute(httpGet); - byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - 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); + HttpGet http = new HttpGet("http://localhost:" + ourPort + "/Binary/foo"); + binaryRead(http); } @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); - byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertEquals("foo", status.getFirstHeader("content-type").getValue()); - assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue()); // This is a security requirement! - assertArrayEquals(new byte[] { 1, 2, 3, 4 }, responseContent); + HttpGet http = new HttpGet("http://localhost:" + ourPort + "/Binary/foo"); + http.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + http.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + + binaryRead(http); } - + + private void binaryRead(HttpGet http) throws IOException { + try (CloseableHttpResponse status = ourClient.execute(http)) { + byte[] responseContent = IOUtils.toByteArray(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("foo", status.getFirstHeader("content-type").getValue()); + assertEquals("Attachment;", status.getFirstHeader("Content-Disposition").getValue()); // This is a security requirement! + assertArrayEquals(new byte[]{1, 2, 3, 4}, responseContent); + } + } + @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); - String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); - 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); + HttpGet http = new HttpGet("http://localhost:" + ourPort + "/Binary/foo"); + http.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); + http.addHeader("Accept", Constants.CT_FHIR_JSON); + try (CloseableHttpResponse status = ourClient.execute(http)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + IOUtils.closeQuietly(status.getEntity().getContent()); + 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); + } } @Test public void testSearchJson() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary?_pretty=true&_format=json"); - HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").replace("UTF", "utf")); + HttpGet http = new HttpGet("http://localhost:" + ourPort + "/Binary?_pretty=true&_format=json"); + try (CloseableHttpResponse response = ourClient.execute(http)) { + String responseContent = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + IOUtils.closeQuietly(response.getEntity().getContent()); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", response.getFirstHeader("content-type").getValue().replace(" ", "").replace("UTF", "utf")); - ourLog.info(responseContent); + ourLog.info(responseContent); - Bundle bundle = ourCtx.newJsonParser().parseResource(Bundle.class, responseContent); - Binary bin = (Binary) bundle.getEntry().get(0).getResource(); + Bundle bundle = ourCtx.newJsonParser().parseResource(Bundle.class, responseContent); + Binary bin = (Binary) bundle.getEntry().get(0).getResource(); - assertEquals("text/plain", bin.getContentType()); - assertArrayEquals(new byte[] { 1, 2, 3, 4 }, bin.getContent()); + assertEquals("text/plain", bin.getContentType()); + assertArrayEquals(new byte[]{1, 2, 3, 4}, bin.getContent()); + } } @Test public void testSearchXml() throws Exception { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Binary?_pretty=true"); - HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); - IOUtils.closeQuietly(status.getEntity().getContent()); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertEquals(Constants.CT_FHIR_XML + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").replace("UTF", "utf")); + HttpGet http = new HttpGet("http://localhost:" + ourPort + "/Binary?_pretty=true"); + try (CloseableHttpResponse response = ourClient.execute(http)) { + String responseContent = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + IOUtils.closeQuietly(response.getEntity().getContent()); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertEquals(Constants.CT_FHIR_XML + ";charset=utf-8", response.getFirstHeader("content-type").getValue().replace(" ", "").replace("UTF", "utf")); - ourLog.info(responseContent); + ourLog.info(responseContent); - Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent); - Binary bin = (Binary) bundle.getEntry().get(0).getResource(); - - assertEquals("text/plain", bin.getContentType()); - assertArrayEquals(new byte[] { 1, 2, 3, 4 }, bin.getContent()); - } - - @BeforeClass - public static void beforeClass() throws Exception { - ourPort = PortUtil.findFreePort(); - ourServer = new Server(ourPort); - - ResourceProvider patientProvider = new ResourceProvider(); - - ServletHandler proxyHandler = new ServletHandler(); - RestfulServer servlet = new RestfulServer(ourCtx); - servlet.setResourceProviders(patientProvider); - 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(); + Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent); + Binary bin = (Binary) bundle.getEntry().get(0).getResource(); + assertEquals("text/plain", bin.getContentType()); + assertArrayEquals(new byte[]{1, 2, 3, 4}, bin.getContent()); + } } public static class ResourceProvider implements IResourceProvider { @@ -260,7 +272,5 @@ public class BinaryDstu2Test { retVal.setContentType("text/plain"); return Collections.singletonList(retVal); } - } - } 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 295fc423344..fe70c2e20bb 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 @@ -1769,6 +1769,7 @@ public class AuthorizationInterceptorDstu2Test { @Override public List buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() + .allow().patch().allRequests().andThen() .allow("Rule 1").write().instance("Patient/900").andThen() .build(); } @@ -1788,13 +1789,6 @@ public class AuthorizationInterceptorDstu2Test { assertEquals(204, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); - ourHitMethod = false; - httpPost = new HttpPatch("http://localhost:" + ourPort + "/Patient/999"); - httpPost.setEntity(new StringEntity(input, ContentType.parse("application/json-patch+json"))); - status = ourClient.execute(httpPost); - response = extractResponseAndClose(status); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); } @Test diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 45b430c375a..a36ee3530f5 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -146,7 +146,6 @@ 1.1 - xpp3 xpp3 diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/HapiWorkerContext.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/HapiWorkerContext.java index 44838b0a445..cbc74d9ea9e 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/HapiWorkerContext.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/ctx/HapiWorkerContext.java @@ -19,7 +19,6 @@ import org.hl7.fhir.dstu3.terminologies.ValueSetExpanderFactory; import org.hl7.fhir.dstu3.terminologies.ValueSetExpanderSimple; import org.hl7.fhir.dstu3.utils.INarrativeGenerator; import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.exceptions.TerminologyServiceException; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import java.util.*; @@ -28,311 +27,310 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; public final class HapiWorkerContext implements IWorkerContext, ValueSetExpander, ValueSetExpanderFactory { - private final FhirContext myCtx; - private Map myFetchedResourceCache = new HashMap(); - private IValidationSupport myValidationSupport; - private ExpansionProfile myExpansionProfile; + private final FhirContext myCtx; + private Map myFetchedResourceCache = new HashMap(); + private IValidationSupport myValidationSupport; + private ExpansionProfile myExpansionProfile; - public HapiWorkerContext(FhirContext theCtx, IValidationSupport theValidationSupport) { - Validate.notNull(theCtx, "theCtx must not be null"); - Validate.notNull(theValidationSupport, "theValidationSupport must not be null"); - myCtx = theCtx; - myValidationSupport = theValidationSupport; - } + public HapiWorkerContext(FhirContext theCtx, IValidationSupport theValidationSupport) { + Validate.notNull(theCtx, "theCtx must not be null"); + Validate.notNull(theValidationSupport, "theValidationSupport must not be null"); + myCtx = theCtx; + myValidationSupport = theValidationSupport; + } - @Override - public List allStructures() { - return myValidationSupport.fetchAllStructureDefinitions(myCtx); - } + @Override + @CoverageIgnore + public List allConformanceResources() { + throw new UnsupportedOperationException(); + } - @Override - public CodeSystem fetchCodeSystem(String theSystem) { - if (myValidationSupport == null) { - return null; - } else { - return myValidationSupport.fetchCodeSystem(myCtx, theSystem); - } - } + @Override + public List allStructures() { + return myValidationSupport.fetchAllStructureDefinitions(myCtx); + } - @Override - public T fetchResource(Class theClass, String theUri) { - if (myValidationSupport == null) { - return null; - } else { - @SuppressWarnings("unchecked") - T retVal = (T) myFetchedResourceCache.get(theUri); - if (retVal == null) { - retVal = myValidationSupport.fetchResource(myCtx, theClass, theUri); - if (retVal != null) { - myFetchedResourceCache.put(theUri, retVal); - } - } - return retVal; - } - } + @Override + public ValueSetExpansionOutcome expand(ValueSet theSource, ExpansionProfile theProfile) { + ValueSetExpansionOutcome vso; + try { + vso = getExpander().expand(theSource, theProfile); + } catch (InvalidRequestException e) { + throw e; + } catch (Exception e) { + throw new InternalErrorException(e); + } + if (vso.getError() != null) { + throw new InvalidRequestException(vso.getError()); + } else { + return vso; + } + } - @Override - public List findMapsForSource(String theUrl) { - throw new UnsupportedOperationException(); - } + @Override + public ValueSetExpansionOutcome expandVS(ValueSet theSource, boolean theCacheOk, boolean theHeiarchical) { + throw new UnsupportedOperationException(); + } - @Override - public String getAbbreviation(String theName) { - throw new UnsupportedOperationException(); - } + @Override + public ValueSetExpansionComponent expandVS(ConceptSetComponent theInc, boolean theHeiarchical) { + return myValidationSupport.expandValueSet(myCtx, theInc); + } - @Override - public ValueSetExpander getExpander() { - ValueSetExpanderSimple retVal = new ValueSetExpanderSimple(this, this); - retVal.setMaxExpansionSize(Integer.MAX_VALUE); - return retVal; - } + @Override + public CodeSystem fetchCodeSystem(String theSystem) { + if (myValidationSupport == null) { + return null; + } else { + return myValidationSupport.fetchCodeSystem(myCtx, theSystem); + } + } - @Override - public INarrativeGenerator getNarrativeGenerator(String thePrefix, String theBasePath) { - throw new UnsupportedOperationException(); - } + @Override + public T fetchResource(Class theClass, String theUri) { + if (myValidationSupport == null) { + return null; + } else { + @SuppressWarnings("unchecked") + T retVal = (T) myFetchedResourceCache.get(theUri); + if (retVal == null) { + retVal = myValidationSupport.fetchResource(myCtx, theClass, theUri); + if (retVal != null) { + myFetchedResourceCache.put(theUri, retVal); + } + } + return retVal; + } + } - @Override - public IParser getParser(ParserType theType) { - throw new UnsupportedOperationException(); - } + @Override + public T fetchResourceWithException(Class theClass_, String theUri) throws FHIRException { + T retVal = fetchResource(theClass_, theUri); + if (retVal == null) { + throw new FHIRException("Unable to fetch " + theUri); + } + return retVal; + } - @Override - public IParser getParser(String theType) { - throw new UnsupportedOperationException(); - } + @Override + public List findMapsForSource(String theUrl) { + throw new UnsupportedOperationException(); + } - @Override - public List getResourceNames() { - List result = new ArrayList(); - for (ResourceType next : ResourceType.values()) { - result.add(next.name()); - } - Collections.sort(result); - return result; - } + @Override + public String getAbbreviation(String theName) { + throw new UnsupportedOperationException(); + } - @Override - public boolean hasResource(Class theClass_, String theUri) { - throw new UnsupportedOperationException(); - } + @Override + public ValueSetExpander getExpander() { + ValueSetExpanderSimple retVal = new ValueSetExpanderSimple(this, this); + retVal.setMaxExpansionSize(Integer.MAX_VALUE); + return retVal; + } - @Override - public IParser newJsonParser() { - throw new UnsupportedOperationException(); - } + @Override + public ExpansionProfile getExpansionProfile() { + return myExpansionProfile; + } + @Override + public void setExpansionProfile(ExpansionProfile theExpProfile) { + myExpansionProfile = theExpProfile; + } - @Override - public IParser newXmlParser() { - throw new UnsupportedOperationException(); - } + @Override + public INarrativeGenerator getNarrativeGenerator(String thePrefix, String theBasePath) { + throw new UnsupportedOperationException(); + } - @Override - public String oid2Uri(String theCode) { - throw new UnsupportedOperationException(); - } + @Override + public IParser getParser(ParserType theType) { + throw new UnsupportedOperationException(); + } - @Override - public boolean supportsSystem(String theSystem) { - if (myValidationSupport == null) { - return false; - } else { - return myValidationSupport.isCodeSystemSupported(myCtx, theSystem); - } - } + @Override + public IParser getParser(String theType) { + throw new UnsupportedOperationException(); + } - @Override - public Set typeTails() { - return new HashSet(Arrays.asList("Integer", "UnsignedInt", "PositiveInt", "Decimal", "DateTime", "Date", "Time", "Instant", "String", "Uri", "Oid", "Uuid", "Id", "Boolean", "Code", - "Markdown", "Base64Binary", "Coding", "CodeableConcept", "Attachment", "Identifier", "Quantity", "SampledData", "Range", "Period", "Ratio", "HumanName", "Address", "ContactPoint", - "Timing", "Reference", "Annotation", "Signature", "Meta")); - } - - @Override - public ValidationResult validateCode(CodeableConcept theCode, ValueSet theVs) { - for (Coding next : theCode.getCoding()) { - ValidationResult retVal = validateCode(next, theVs); - if (retVal != null && retVal.isOk()) { - return retVal; - } - } - - return new ValidationResult(null, null); - } - - @Override - public ValidationResult validateCode(Coding theCode, ValueSet theVs) { - String system = theCode.getSystem(); - String code = theCode.getCode(); - String display = theCode.getDisplay(); - return validateCode(system, code, display, theVs); - } - - @Override - public ValidationResult validateCode(String theSystem, String theCode, String theDisplay) { - IValidationSupport.CodeValidationResult result = myValidationSupport.validateCode(myCtx, theSystem, theCode, theDisplay); - if (result == null) { - return null; - } - return new ValidationResult(result.getSeverity(), result.getMessage(), result.asConceptDefinition()); - } - - @Override - public ValidationResult validateCode(String theSystem, String theCode, String theDisplay, ConceptSetComponent theVsi) { - throw new UnsupportedOperationException(); - } - - @Override - public ValidationResult validateCode(String theSystem, String theCode, String theDisplay, ValueSet theVs) { - - if (theVs != null && isNotBlank(theCode)) { - for (ConceptSetComponent next : theVs.getCompose().getInclude()) { - if (isBlank(theSystem) || theSystem.equals(next.getSystem())) { - for (ConceptReferenceComponent nextCode : next.getConcept()) { - if (theCode.equals(nextCode.getCode())) { - CodeType code = new CodeType(theCode); - return new ValidationResult(new ConceptDefinitionComponent(code)); - } - } - } - } - } - - - boolean caseSensitive = true; - if (isNotBlank(theSystem)) { - CodeSystem system = fetchCodeSystem(theSystem); - if (system == null) { - return new ValidationResult(IssueSeverity.INFORMATION, "Code " + theSystem + "/" + theCode + " was not validated because the code system is not present"); - } - - if (system.hasCaseSensitive()) { - caseSensitive = system.getCaseSensitive(); - } - } - - String wantCode = theCode; - if (!caseSensitive) { - wantCode = wantCode.toUpperCase(); - } - - ValueSetExpansionOutcome expandedValueSet = null; - - /* - * The following valueset is a special case, since the BCP codesystem is very difficult to expand - */ - if (theVs != null && "http://hl7.org/fhir/ValueSet/languages".equals(theVs.getId())) { - ValueSet expansion = new ValueSet(); - for (ConceptSetComponent nextInclude : theVs.getCompose().getInclude()) { - for (ConceptReferenceComponent nextConcept : nextInclude.getConcept()) { - expansion.getExpansion().addContains().setCode(nextConcept.getCode()).setDisplay(nextConcept.getDisplay()); - } - } - expandedValueSet = new ValueSetExpansionOutcome(expansion); - } - - if (expandedValueSet == null) { - expandedValueSet = expand(theVs, null); - } - - for (ValueSetExpansionContainsComponent next : expandedValueSet.getValueset().getExpansion().getContains()) { - String nextCode = next.getCode(); - if (!caseSensitive) { - nextCode = nextCode.toUpperCase(); - } - - if (nextCode.equals(wantCode)) { - if (theSystem == null || next.getSystem().equals(theSystem)) { - ConceptDefinitionComponent definition = new ConceptDefinitionComponent(); - definition.setCode(next.getCode()); - definition.setDisplay(next.getDisplay()); - ValidationResult retVal = new ValidationResult(definition); - return retVal; - } - } - } - - return new ValidationResult(IssueSeverity.ERROR, "Unknown code[" + theCode + "] in system[" + theSystem + "]"); - } - - @Override - @CoverageIgnore - public List allConformanceResources() { - throw new UnsupportedOperationException(); - } - - @Override - @CoverageIgnore - public boolean hasCache() { - throw new UnsupportedOperationException(); - } - - @Override - public ValueSetExpansionOutcome expand(ValueSet theSource, ExpansionProfile theProfile) { - ValueSetExpansionOutcome vso; - try { - vso = getExpander().expand(theSource, theProfile); - } catch (InvalidRequestException e) { - throw e; - } catch (Exception e) { - throw new InternalErrorException(e); - } - if (vso.getError() != null) { - throw new InvalidRequestException(vso.getError()); - } else { - return vso; - } - } - - @Override - public ExpansionProfile getExpansionProfile() { - return myExpansionProfile; - } - - @Override - public void setExpansionProfile(ExpansionProfile theExpProfile) { - myExpansionProfile = theExpProfile; - } - - @Override - public ValueSetExpansionOutcome expandVS(ValueSet theSource, boolean theCacheOk, boolean theHeiarchical) { - throw new UnsupportedOperationException(); - } - - @Override - public ValueSetExpansionComponent expandVS(ConceptSetComponent theInc, boolean theHeiarchical) throws TerminologyServiceException { - return myValidationSupport.expandValueSet(myCtx, theInc); - } - - @Override - public void setLogger(ILoggingService theLogger) { - throw new UnsupportedOperationException(); - } - - @Override - public String getVersion() { - return myCtx.getVersion().getVersion().getFhirVersionString(); - } - - @Override - public boolean isNoTerminologyServer() { - return false; - } - - @Override - public T fetchResourceWithException(Class theClass_, String theUri) throws FHIRException { - T retVal = fetchResource(theClass_, theUri); - if (retVal == null) { - throw new FHIRException("Unable to fetch " + theUri); - } - return retVal; - } + @Override + public List getResourceNames() { + List result = new ArrayList(); + for (ResourceType next : ResourceType.values()) { + result.add(next.name()); + } + Collections.sort(result); + return result; + } @Override public List getTypeNames() { throw new UnsupportedOperationException(); } + @Override + public String getVersion() { + return myCtx.getVersion().getVersion().getFhirVersionString(); + } + + @Override + @CoverageIgnore + public boolean hasCache() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasResource(Class theClass_, String theUri) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isNoTerminologyServer() { + return false; + } + + @Override + public IParser newJsonParser() { + throw new UnsupportedOperationException(); + } + + @Override + public IParser newXmlParser() { + throw new UnsupportedOperationException(); + } + + @Override + public String oid2Uri(String theCode) { + throw new UnsupportedOperationException(); + } + + @Override + public void setLogger(ILoggingService theLogger) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean supportsSystem(String theSystem) { + if (myValidationSupport == null) { + return false; + } else { + return myValidationSupport.isCodeSystemSupported(myCtx, theSystem); + } + } + + @Override + public Set typeTails() { + return new HashSet(Arrays.asList("Integer", "UnsignedInt", "PositiveInt", "Decimal", "DateTime", "Date", "Time", "Instant", "String", "Uri", "Oid", "Uuid", "Id", "Boolean", "Code", + "Markdown", "Base64Binary", "Coding", "CodeableConcept", "Attachment", "Identifier", "Quantity", "SampledData", "Range", "Period", "Ratio", "HumanName", "Address", "ContactPoint", + "Timing", "Reference", "Annotation", "Signature", "Meta")); + } + + @Override + public ValidationResult validateCode(CodeableConcept theCode, ValueSet theVs) { + for (Coding next : theCode.getCoding()) { + ValidationResult retVal = validateCode(next, theVs); + if (retVal != null && retVal.isOk()) { + return retVal; + } + } + + return new ValidationResult(null, null); + } + + @Override + public ValidationResult validateCode(Coding theCode, ValueSet theVs) { + String system = theCode.getSystem(); + String code = theCode.getCode(); + String display = theCode.getDisplay(); + return validateCode(system, code, display, theVs); + } + + @Override + public ValidationResult validateCode(String theSystem, String theCode, String theDisplay) { + IValidationSupport.CodeValidationResult result = myValidationSupport.validateCode(myCtx, theSystem, theCode, theDisplay); + if (result == null) { + return null; + } + return new ValidationResult(result.getSeverity(), result.getMessage(), result.asConceptDefinition()); + } + + @Override + public ValidationResult validateCode(String theSystem, String theCode, String theDisplay, ConceptSetComponent theVsi) { + throw new UnsupportedOperationException(); + } + + @Override + public ValidationResult validateCode(String theSystem, String theCode, String theDisplay, ValueSet theVs) { + + if (theVs != null && isNotBlank(theCode)) { + for (ConceptSetComponent next : theVs.getCompose().getInclude()) { + if (isBlank(theSystem) || theSystem.equals(next.getSystem())) { + for (ConceptReferenceComponent nextCode : next.getConcept()) { + if (theCode.equals(nextCode.getCode())) { + CodeType code = new CodeType(theCode); + return new ValidationResult(new ConceptDefinitionComponent(code)); + } + } + } + } + } + + + boolean caseSensitive = true; + if (isNotBlank(theSystem)) { + CodeSystem system = fetchCodeSystem(theSystem); + if (system == null) { + return new ValidationResult(IssueSeverity.INFORMATION, "Code " + theSystem + "/" + theCode + " was not validated because the code system is not present"); + } + + if (system.hasCaseSensitive()) { + caseSensitive = system.getCaseSensitive(); + } + } + + String wantCode = theCode; + if (!caseSensitive) { + wantCode = wantCode.toUpperCase(); + } + + ValueSetExpansionOutcome expandedValueSet = null; + + /* + * The following valueset is a special case, since the BCP codesystem is very difficult to expand + */ + if (theVs != null && "http://hl7.org/fhir/ValueSet/languages".equals(theVs.getId())) { + ValueSet expansion = new ValueSet(); + for (ConceptSetComponent nextInclude : theVs.getCompose().getInclude()) { + for (ConceptReferenceComponent nextConcept : nextInclude.getConcept()) { + expansion.getExpansion().addContains().setCode(nextConcept.getCode()).setDisplay(nextConcept.getDisplay()); + } + } + expandedValueSet = new ValueSetExpansionOutcome(expansion); + } + + if (expandedValueSet == null) { + expandedValueSet = expand(theVs, null); + } + + for (ValueSetExpansionContainsComponent next : expandedValueSet.getValueset().getExpansion().getContains()) { + String nextCode = next.getCode(); + if (!caseSensitive) { + nextCode = nextCode.toUpperCase(); + } + + if (nextCode.equals(wantCode)) { + if (theSystem == null || next.getSystem().equals(theSystem)) { + ConceptDefinitionComponent definition = new ConceptDefinitionComponent(); + definition.setCode(next.getCode()); + definition.setDisplay(next.getDisplay()); + ValidationResult retVal = new ValidationResult(definition); + return retVal; + } + } + } + + return new ValidationResult(IssueSeverity.ERROR, "Unknown code[" + theCode + "] in system[" + theSystem + "]"); + } + } diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/FHIRPathEngine.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/FHIRPathEngine.java index f2ef48ebc89..713164ddcb7 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/FHIRPathEngine.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/FHIRPathEngine.java @@ -25,9 +25,7 @@ import java.util.*; import static org.apache.commons.lang3.StringUtils.length; /** - * * @author Grahame Grieve - * */ public class FHIRPathEngine { private IWorkerContext worker; @@ -36,87 +34,6 @@ public class FHIRPathEngine { private Set primitiveTypes = new HashSet(); private Map allTypes = new HashMap(); - // if the fhir path expressions are allowed to use constants beyond those defined in the specification - // the application can implement them by providing a constant resolver - public interface IEvaluationContext { - public class FunctionDetails { - private String description; - private int minParameters; - private int maxParameters; - public FunctionDetails(String description, int minParameters, int maxParameters) { - super(); - this.description = description; - this.minParameters = minParameters; - this.maxParameters = maxParameters; - } - public String getDescription() { - return description; - } - public int getMinParameters() { - return minParameters; - } - public int getMaxParameters() { - return maxParameters; - } - - } - - /** - * A constant reference - e.g. a reference to a name that must be resolved in context. - * The % will be removed from the constant name before this is invoked. - * - * This will also be called if the host invokes the FluentPath engine with a context of null - * - * @param appContext - content passed into the fluent path engine - * @param name - name reference to resolve - * @return the value of the reference (or null, if it's not valid, though can throw an exception if desired) - */ - public Base resolveConstant(Object appContext, String name) throws PathEngineException; - public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException; - - /** - * when the .log() function is called - * - * @param argument - * @param focus - * @return - */ - public boolean log(String argument, List focus); - - // extensibility for functions - /** - * - * @param functionName - * @return null if the function is not known - */ - public FunctionDetails resolveFunction(String functionName); - - /** - * Check the function parameters, and throw an error if they are incorrect, or return the type for the function - * @param functionName - * @param parameters - * @return - */ - public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException; - - /** - * @param appContext - * @param functionName - * @param parameters - * @return - */ - public List executeFunction(Object appContext, String functionName, List> parameters); - - /** - * Implementation of resolve() function. Passed a string, return matching resource, if one is known - else null - * @param appInfo - * @param url - * @return - */ - public Base resolveReference(Object appContext, String url); - } - - /** * @param worker - used when validating paths (@check), and used doing value set membership when executing tests (once that's defined) */ @@ -132,123 +49,71 @@ public class FHIRPathEngine { } } + private TypeDetails anything(CollectionStatus status) { + return new TypeDetails(status, allTypes.keySet()); + } + // --- 3 methods to override in children ------------------------------------------------------- // if you don't override, it falls through to the using the base reference implementation // HAPI overrides to these to support extending the base model - public IEvaluationContext getHostServices() { - return hostServices; + private ExecutionContext changeThis(ExecutionContext context, Base newThis) { + return new ExecutionContext(context.appInfo, context.resource, context.context, context.aliases, newThis); } - - public void setHostServices(IEvaluationContext constantResolver) { - this.hostServices = constantResolver; - } - - - /** - * Given an item, return all the children that conform to the pattern described in name - * - * Possible patterns: - * - a simple name (which may be the base of a name with [] e.g. value[x]) - * - a name with a type replacement e.g. valueCodeableConcept - * - * which means all children - * - ** which means all descendants - * - * @param item - * @param name - * @param result - * @throws FHIRException - */ - protected void getChildrenByName(Base item, String name, List result) throws FHIRException { - Base[] list = item.listChildrenByName(name, false); - if (list != null) - for (Base v : list) - if (v != null) - result.add(v); - } - - // --- public API ------------------------------------------------------- - /** - * Parse a path for later use using execute - * - * @param path - * @return - * @throws PathEngineException - * @throws Exception - */ - public ExpressionNode parse(String path) throws FHIRLexerException { - FHIRLexer lexer = new FHIRLexer(path); - if (lexer.done()) - throw lexer.error("Path cannot be empty"); - ExpressionNode result = parseExpression(lexer, true); - if (!lexer.done()) - throw lexer.error("Premature ExpressionNode termination at unexpected token \""+lexer.getCurrent()+"\""); - result.check(); - return result; - } - - /** - * Parse a path that is part of some other syntax - * - * @param path - * @return - * @throws PathEngineException - * @throws Exception - */ - public ExpressionNode parse(FHIRLexer lexer) throws FHIRLexerException { - ExpressionNode result = parseExpression(lexer, true); - result.check(); - return result; + private ExecutionTypeContext changeThis(ExecutionTypeContext context, TypeDetails newThis) { + return new ExecutionTypeContext(context.appInfo, context.resource, context.context, newThis); } /** * check that paths referred to in the ExpressionNode are valid - * + *

    * xPathStartsWithValueRef is a hack work around for the fact that FHIR Path sometimes needs a different starting point than the xpath - * + *

    * returns a list of the possible types that might be returned by executing the ExpressionNode against a particular context * * @param context - the logical type against which this path is applied - * @param path - the FHIR Path statement to check + * @param path - the FHIR Path statement to check * @throws DefinitionException * @throws PathEngineException * @if the path is not valid */ public TypeDetails check(Object appContext, String resourceType, String context, ExpressionNode expr) throws FHIRLexerException, PathEngineException, DefinitionException { // if context is a path that refers to a type, do that conversion now - TypeDetails types; - if (context == null) { - types = null; // this is a special case; the first path reference will have to resolve to something in the context - } else if (!context.contains(".")) { - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, context); - types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl()); - } else { - String ctxt = context.substring(0, context.indexOf('.')); + TypeDetails types; + if (context == null) { + types = null; // this is a special case; the first path reference will have to resolve to something in the context + } else if (!context.contains(".")) { + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, context); + types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl()); + } else { + String ctxt = context.substring(0, context.indexOf('.')); if (Utilities.isAbsoluteUrl(resourceType)) { - ctxt = resourceType.substring(0, resourceType.lastIndexOf("/")+1)+ctxt; + ctxt = resourceType.substring(0, resourceType.lastIndexOf("/") + 1) + ctxt; } - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, ctxt); - if (sd == null) - throw new PathEngineException("Unknown context "+context); - ElementDefinitionMatch ed = getElementDefinition(sd, context, true); - if (ed == null) - throw new PathEngineException("Unknown context element "+context); - if (ed.fixedType != null) - types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); - else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) - types = new TypeDetails(CollectionStatus.SINGLETON, ctxt+"#"+context); - else { - types = new TypeDetails(CollectionStatus.SINGLETON); - for (TypeRefComponent t : ed.getDefinition().getType()) - types.addType(t.getCode()); - } - } + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, ctxt); + if (sd == null) + throw new PathEngineException("Unknown context " + context); + ElementDefinitionMatch ed = getElementDefinition(sd, context, true); + if (ed == null) + throw new PathEngineException("Unknown context element " + context); + if (ed.fixedType != null) + types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); + else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) + types = new TypeDetails(CollectionStatus.SINGLETON, ctxt + "#" + context); + else { + types = new TypeDetails(CollectionStatus.SINGLETON); + for (TypeRefComponent t : ed.getDefinition().getType()) + types.addType(t.getCode()); + } + } return executeType(new ExecutionTypeContext(appContext, resourceType, context, types), types, expr, true); } + // --- public API ------------------------------------------------------- + public TypeDetails check(Object appContext, StructureDefinition sd, String context, ExpressionNode expr) throws FHIRLexerException, PathEngineException, DefinitionException { // if context is a path that refers to a type, do that conversion now TypeDetails types; @@ -257,11 +122,11 @@ public class FHIRPathEngine { } else { ElementDefinitionMatch ed = getElementDefinition(sd, context, true); if (ed == null) - throw new PathEngineException("Unknown context element "+context); + throw new PathEngineException("Unknown context element " + context); if (ed.fixedType != null) types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) - types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl()+"#"+context); + types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl() + "#" + context); else { types = new TypeDetails(CollectionStatus.SINGLETON); for (TypeRefComponent t : ed.getDefinition().getType()) @@ -282,17 +147,286 @@ public class FHIRPathEngine { return check(appContext, resourceType, context, parse(expr)); } + private void checkConstant(String s, FHIRLexer lexer) throws FHIRLexerException { + if (s.startsWith("\'") && s.endsWith("\'")) { + int i = 1; + while (i < s.length() - 1) { + char ch = s.charAt(i); + if (ch == '\\') { + switch (ch) { + case 't': + case 'r': + case 'n': + case 'f': + case '\'': + case '\\': + case '/': + i++; + break; + case 'u': + if (!Utilities.isHex("0x" + s.substring(i, i + 4))) + throw lexer.error("Improper unicode escape \\u" + s.substring(i, i + 4)); + break; + default: + throw lexer.error("Unknown character escape \\" + ch); + } + } else + i++; + } + } + } + + private void checkContextCoded(TypeDetails focus, String name) throws PathEngineException { + if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Coding") && !focus.hasType(worker, "CodeableConcept")) + throw new PathEngineException("The function '" + name + "'() can only be used on string, code, uri, Coding, CodeableConcept"); + } + + private void checkContextPrimitive(TypeDetails focus, String name) throws PathEngineException { + if (!focus.hasType(primitiveTypes)) + throw new PathEngineException("The function '" + name + "'() can only be used on " + primitiveTypes.toString()); + } + + private void checkContextReference(TypeDetails focus, String name) throws PathEngineException { + if (!focus.hasType(worker, "string") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Reference")) + throw new PathEngineException("The function '" + name + "'() can only be used on string, uri, Reference"); + } + + private void checkContextString(TypeDetails focus, String name) throws PathEngineException { + if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "id")) + throw new PathEngineException("The function '" + name + "'() can only be used on string, uri, code, id, but found " + focus.describe()); + } + + private void checkOrdered(TypeDetails focus, String name) throws PathEngineException { + if (focus.getCollectionStatus() == CollectionStatus.UNORDERED) + throw new PathEngineException("The function '" + name + "'() can only be used on ordered collections"); + } + + private boolean checkParamCount(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, int count) throws FHIRLexerException { + if (exp.getParameters().size() != count) + throw lexer.error("The function \"" + exp.getName() + "\" requires " + Integer.toString(count) + " parameters", location.toString()); + return true; + } + + private boolean checkParamCount(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, int countMin, int countMax) throws FHIRLexerException { + if (exp.getParameters().size() < countMin || exp.getParameters().size() > countMax) + throw lexer.error("The function \"" + exp.getName() + "\" requires between " + Integer.toString(countMin) + " and " + Integer.toString(countMax) + " parameters", location.toString()); + return true; + } + + private void checkParamTypes(String funcName, List paramTypes, TypeDetails... typeSet) throws PathEngineException { + int i = 0; + for (TypeDetails pt : typeSet) { + if (i == paramTypes.size()) + return; + TypeDetails actual = paramTypes.get(i); + i++; + for (String a : actual.getTypes()) { + if (!pt.hasType(worker, a)) + throw new PathEngineException("The parameter type '" + a + "' is not legal for " + funcName + " parameter " + Integer.toString(i) + ". expecting " + pt.toString()); + } + } + } + + private boolean checkParameters(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, FunctionDetails details) throws FHIRLexerException { + switch (exp.getFunction()) { + case Empty: + return checkParamCount(lexer, location, exp, 0); + case Not: + return checkParamCount(lexer, location, exp, 0); + case Exists: + return checkParamCount(lexer, location, exp, 0); + case SubsetOf: + return checkParamCount(lexer, location, exp, 1); + case SupersetOf: + return checkParamCount(lexer, location, exp, 1); + case IsDistinct: + return checkParamCount(lexer, location, exp, 0); + case Distinct: + return checkParamCount(lexer, location, exp, 0); + case Count: + return checkParamCount(lexer, location, exp, 0); + case Where: + return checkParamCount(lexer, location, exp, 1); + case Select: + return checkParamCount(lexer, location, exp, 1); + case All: + return checkParamCount(lexer, location, exp, 0, 1); + case Repeat: + return checkParamCount(lexer, location, exp, 1); + case Item: + return checkParamCount(lexer, location, exp, 1); + case As: + return checkParamCount(lexer, location, exp, 1); + case Is: + return checkParamCount(lexer, location, exp, 1); + case Single: + return checkParamCount(lexer, location, exp, 0); + case First: + return checkParamCount(lexer, location, exp, 0); + case Last: + return checkParamCount(lexer, location, exp, 0); + case Tail: + return checkParamCount(lexer, location, exp, 0); + case Skip: + return checkParamCount(lexer, location, exp, 1); + case Take: + return checkParamCount(lexer, location, exp, 1); + case Iif: + return checkParamCount(lexer, location, exp, 2, 3); + case ToInteger: + return checkParamCount(lexer, location, exp, 0); + case ToDecimal: + return checkParamCount(lexer, location, exp, 0); + case ToString: + return checkParamCount(lexer, location, exp, 0); + case Substring: + return checkParamCount(lexer, location, exp, 1, 2); + case StartsWith: + return checkParamCount(lexer, location, exp, 1); + case EndsWith: + return checkParamCount(lexer, location, exp, 1); + case Matches: + return checkParamCount(lexer, location, exp, 1); + case ReplaceMatches: + return checkParamCount(lexer, location, exp, 2); + case Contains: + return checkParamCount(lexer, location, exp, 1); + case Replace: + return checkParamCount(lexer, location, exp, 2); + case Length: + return checkParamCount(lexer, location, exp, 0); + case Children: + return checkParamCount(lexer, location, exp, 0); + case Descendants: + return checkParamCount(lexer, location, exp, 0); + case MemberOf: + return checkParamCount(lexer, location, exp, 1); + case Trace: + return checkParamCount(lexer, location, exp, 1); + case Today: + return checkParamCount(lexer, location, exp, 0); + case Now: + return checkParamCount(lexer, location, exp, 0); + case Resolve: + return checkParamCount(lexer, location, exp, 0); + case Extension: + return checkParamCount(lexer, location, exp, 1); + case HasValue: + return checkParamCount(lexer, location, exp, 0); + case Alias: + return checkParamCount(lexer, location, exp, 1); + case AliasAs: + return checkParamCount(lexer, location, exp, 1); + case Custom: + return checkParamCount(lexer, location, exp, details.getMinParameters(), details.getMaxParameters()); + } + return false; + } + + private TypeDetails childTypes(TypeDetails focus, String mask) throws PathEngineException, DefinitionException { + TypeDetails result = new TypeDetails(CollectionStatus.UNORDERED); + for (String f : focus.getTypes()) + getChildTypesByName(f, mask, result); + return result; + } + + private int compareDateTimeElements(Base theL, Base theR) { + String dateLeftString = theL.primitiveValue(); + if (length(dateLeftString) > 10) { + DateTimeType dateLeft = new DateTimeType(dateLeftString); + dateLeft.setTimeZoneZulu(true); + dateLeftString = dateLeft.getValueAsString(); + } + String dateRightString = theR.primitiveValue(); + if (length(dateRightString) > 10) { + DateTimeType dateRight = new DateTimeType(dateRightString); + dateRight.setTimeZoneZulu(true); + dateRightString = dateRight.getValueAsString(); + } + return dateLeftString.compareTo(dateRightString); + } + + /** + * worker routine for converting a set of objects to a boolean representation (for invariants) + * + * @param items - result from @evaluate + * @return + */ + public boolean convertToBoolean(List items) { + if (items == null) + return false; + else if (items.size() == 1 && items.get(0) instanceof BooleanType) + return ((BooleanType) items.get(0)).getValue(); + else + return items.size() > 0; + } + + /** + * worker routine for converting a set of objects to a string representation + * + * @param items - result from @evaluate + * @return + */ + public String convertToString(List items) { + StringBuilder b = new StringBuilder(); + boolean first = true; + for (Base item : items) { + if (first) + first = false; + else + b.append(','); + + b.append(convertToString(item)); + } + return b.toString(); + } + + private String convertToString(Base item) { + if (item.isPrimitive()) + return item.primitiveValue(); + else + return item.toString(); + } + + private boolean doContains(List list, Base item) { + for (Base test : list) + if (doEquals(test, item)) + return true; + return false; + } + + private boolean doEquals(Base left, Base right) { + if (left.isPrimitive() && right.isPrimitive()) + return Base.equals(left.primitiveValue(), right.primitiveValue()); + else + return Base.compareDeep(left, right, false); + } + + private boolean doEquivalent(Base left, Base right) throws PathEngineException { + if (left.hasType("integer") && right.hasType("integer")) + return doEquals(left, right); + if (left.hasType("boolean") && right.hasType("boolean")) + return doEquals(left, right); + if (left.hasType("integer", "decimal", "unsignedInt", "positiveInt") && right.hasType("integer", "decimal", "unsignedInt", "positiveInt")) + return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); + if (left.hasType("date", "dateTime", "time", "instant") && right.hasType("date", "dateTime", "time", "instant")) + return compareDateTimeElements(left, right) == 0; + if (left.hasType("string", "id", "code", "uri") && right.hasType("string", "id", "code", "uri")) + return Utilities.equivalent(convertToString(left), convertToString(right)); + + throw new PathEngineException(String.format("Unable to determine equivalence between %s and %s", left.fhirType(), right.fhirType())); + } /** * evaluate a path and return the matching elements * - * @param base - the object against which the path is being evaluated + * @param base - the object against which the path is being evaluated * @param ExpressionNode - the parsed ExpressionNode statement to use * @return * @throws FHIRException * @ */ - public List evaluate(Base base, ExpressionNode ExpressionNode) throws FHIRException { + public List evaluate(Base base, ExpressionNode ExpressionNode) throws FHIRException { List list = new ArrayList(); if (base != null) list.add(base); @@ -306,10 +440,10 @@ public class FHIRPathEngine { * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ - public List evaluate(Base base, String path) throws FHIRException { + public List evaluate(Base base, String path) throws FHIRException { ExpressionNode exp = parse(path); List list = new ArrayList(); if (base != null) @@ -321,13 +455,13 @@ public class FHIRPathEngine { /** * evaluate a path and return the matching elements * - * @param base - the object against which the path is being evaluated + * @param base - the object against which the path is being evaluated * @param ExpressionNode - the parsed ExpressionNode statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ - public List evaluate(Object appContext, Resource resource, Base base, ExpressionNode ExpressionNode) throws FHIRException { + public List evaluate(Object appContext, Resource resource, Base base, ExpressionNode ExpressionNode) throws FHIRException { List list = new ArrayList(); if (base != null) list.add(base); @@ -338,7 +472,7 @@ public class FHIRPathEngine { /** * evaluate a path and return the matching elements * - * @param base - the object against which the path is being evaluated + * @param base - the object against which the path is being evaluated * @param ExpressionNode - the parsed ExpressionNode statement to use * @return * @throws FHIRException @@ -358,10 +492,10 @@ public class FHIRPathEngine { * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ - public List evaluate(Object appContext, Resource resource, Base base, String path) throws FHIRException { + public List evaluate(Object appContext, Resource resource, Base base, String path) throws FHIRException { ExpressionNode exp = parse(path); List list = new ArrayList(); if (base != null) @@ -370,19 +504,304 @@ public class FHIRPathEngine { return execute(new ExecutionContext(appContext, resource, base, null, base), list, exp, true); } + private List evaluateFunction(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + switch (exp.getFunction()) { + case Empty: + return funcEmpty(context, focus, exp); + case Not: + return funcNot(context, focus, exp); + case Exists: + return funcExists(context, focus, exp); + case SubsetOf: + return funcSubsetOf(context, focus, exp); + case SupersetOf: + return funcSupersetOf(context, focus, exp); + case IsDistinct: + return funcIsDistinct(context, focus, exp); + case Distinct: + return funcDistinct(context, focus, exp); + case Count: + return funcCount(context, focus, exp); + case Where: + return funcWhere(context, focus, exp); + case Select: + return funcSelect(context, focus, exp); + case All: + return funcAll(context, focus, exp); + case Repeat: + return funcRepeat(context, focus, exp); + case Item: + return funcItem(context, focus, exp); + case As: + return funcAs(context, focus, exp); + case Is: + return funcIs(context, focus, exp); + case Single: + return funcSingle(context, focus, exp); + case First: + return funcFirst(context, focus, exp); + case Last: + return funcLast(context, focus, exp); + case Tail: + return funcTail(context, focus, exp); + case Skip: + return funcSkip(context, focus, exp); + case Take: + return funcTake(context, focus, exp); + case Iif: + return funcIif(context, focus, exp); + case ToInteger: + return funcToInteger(context, focus, exp); + case ToDecimal: + return funcToDecimal(context, focus, exp); + case ToString: + return funcToString(context, focus, exp); + case Substring: + return funcSubstring(context, focus, exp); + case StartsWith: + return funcStartsWith(context, focus, exp); + case EndsWith: + return funcEndsWith(context, focus, exp); + case Matches: + return funcMatches(context, focus, exp); + case ReplaceMatches: + return funcReplaceMatches(context, focus, exp); + case Contains: + return funcContains(context, focus, exp); + case Replace: + return funcReplace(context, focus, exp); + case Length: + return funcLength(context, focus, exp); + case Children: + return funcChildren(context, focus, exp); + case Descendants: + return funcDescendants(context, focus, exp); + case MemberOf: + return funcMemberOf(context, focus, exp); + case Trace: + return funcTrace(context, focus, exp); + case Today: + return funcToday(context, focus, exp); + case Now: + return funcNow(context, focus, exp); + case Resolve: + return funcResolve(context, focus, exp); + case Extension: + return funcExtension(context, focus, exp); + case HasValue: + return funcHasValue(context, focus, exp); + case AliasAs: + return funcAliasAs(context, focus, exp); + case Alias: + return funcAlias(context, focus, exp); + case Custom: { + List> params = new ArrayList>(); + for (ExpressionNode p : exp.getParameters()) + params.add(execute(context, focus, p, true)); + return hostServices.executeFunction(context.appInfo, exp.getName(), params); + } + default: + throw new Error("not Implemented yet"); + } + } + + @SuppressWarnings("unchecked") + private TypeDetails evaluateFunctionType(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp) throws PathEngineException, DefinitionException { + List paramTypes = new ArrayList(); + if (exp.getFunction() == Function.Is || exp.getFunction() == Function.As) + paramTypes.add(new TypeDetails(CollectionStatus.SINGLETON, "string")); + else + for (ExpressionNode expr : exp.getParameters()) { + if (exp.getFunction() == Function.Where || exp.getFunction() == Function.Select || exp.getFunction() == Function.Repeat) + paramTypes.add(executeType(changeThis(context, focus), focus, expr, true)); + else + paramTypes.add(executeType(context, focus, expr, true)); + } + switch (exp.getFunction()) { + case Empty: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Not: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Exists: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case SubsetOf: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + } + case SupersetOf: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + } + case IsDistinct: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Distinct: + return focus; + case Count: + return new TypeDetails(CollectionStatus.SINGLETON, "integer"); + case Where: + return focus; + case Select: + return anything(focus.getCollectionStatus()); + case All: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Repeat: + return anything(focus.getCollectionStatus()); + case Item: { + checkOrdered(focus, "item"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); + return focus; + } + case As: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, exp.getParameters().get(0).getName()); + } + case Is: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + } + case Single: + return focus.toSingleton(); + case First: { + checkOrdered(focus, "first"); + return focus.toSingleton(); + } + case Last: { + checkOrdered(focus, "last"); + return focus.toSingleton(); + } + case Tail: { + checkOrdered(focus, "tail"); + return focus; + } + case Skip: { + checkOrdered(focus, "skip"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); + return focus; + } + case Take: { + checkOrdered(focus, "take"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); + return focus; + } + case Iif: { + TypeDetails types = new TypeDetails(null); + types.update(paramTypes.get(0)); + if (paramTypes.size() > 1) + types.update(paramTypes.get(1)); + return types; + } + case ToInteger: { + checkContextPrimitive(focus, "toInteger"); + return new TypeDetails(CollectionStatus.SINGLETON, "integer"); + } + case ToDecimal: { + checkContextPrimitive(focus, "toDecimal"); + return new TypeDetails(CollectionStatus.SINGLETON, "decimal"); + } + case ToString: { + checkContextPrimitive(focus, "toString"); + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + } + case Substring: { + checkContextString(focus, "subString"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer"), new TypeDetails(CollectionStatus.SINGLETON, "integer")); + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + } + case StartsWith: { + checkContextString(focus, "startsWith"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + } + case EndsWith: { + checkContextString(focus, "endsWith"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + } + case Matches: { + checkContextString(focus, "matches"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + } + case ReplaceMatches: { + checkContextString(focus, "replaceMatches"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + } + case Contains: { + checkContextString(focus, "contains"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + } + case Replace: { + checkContextString(focus, "replace"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + } + case Length: { + checkContextPrimitive(focus, "length"); + return new TypeDetails(CollectionStatus.SINGLETON, "integer"); + } + case Children: + return childTypes(focus, "*"); + case Descendants: + return childTypes(focus, "**"); + case MemberOf: { + checkContextCoded(focus, "memberOf"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + } + case Trace: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return focus; + } + case Today: + return new TypeDetails(CollectionStatus.SINGLETON, "date"); + case Now: + return new TypeDetails(CollectionStatus.SINGLETON, "dateTime"); + case Resolve: { + checkContextReference(focus, "resolve"); + return new TypeDetails(CollectionStatus.SINGLETON, "DomainResource"); + } + case Extension: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "Extension"); + } + case HasValue: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Alias: + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return anything(CollectionStatus.SINGLETON); + case AliasAs: + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return focus; + case Custom: { + return hostServices.checkFunction(context.appInfo, exp.getName(), paramTypes); + } + default: + break; + } + throw new Error("not Implemented yet"); + } + /** * evaluate a path and return true or false (e.g. for an invariant) * * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ - public boolean evaluateToBoolean(Resource resource, Base base, String path) throws FHIRException { + public boolean evaluateToBoolean(Resource resource, Base base, String path) throws FHIRException { return convertToBoolean(evaluate(null, resource, base, path)); } + // procedure CheckParamCount(c : integer); + // begin + // if exp.Parameters.Count <> c then + // raise lexer.error('The function "'+exp.name+'" requires '+inttostr(c)+' parameters', offset); + // end; + /** * evaluate a path and return true or false (e.g. for an invariant) * @@ -425,7 +844,7 @@ public class FHIRPathEngine { * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public String evaluateToString(Base base, String path) throws FHIRException { @@ -436,242 +855,673 @@ public class FHIRPathEngine { return convertToString(evaluate(appInfo, resource, base, node)); } - /** - * worker routine for converting a set of objects to a string representation - * - * @param items - result from @evaluate - * @return - */ - public String convertToString(List items) { - StringBuilder b = new StringBuilder(); - boolean first = true; - for (Base item : items) { - if (first) - first = false; - else - b.append(','); - - b.append(convertToString(item)); - } - return b.toString(); - } - - private String convertToString(Base item) { - if (item.isPrimitive()) - return item.primitiveValue(); - else - return item.toString(); - } - - /** - * worker routine for converting a set of objects to a boolean representation (for invariants) - * - * @param items - result from @evaluate - * @return - */ - public boolean convertToBoolean(List items) { - if (items == null) - return false; - else if (items.size() == 1 && items.get(0) instanceof BooleanType) - return ((BooleanType) items.get(0)).getValue(); - else - return items.size() > 0; - } - - - private void log(String name, List contents) { - if (hostServices == null || !hostServices.log(name, contents)) { - if (log.length() > 0) - log.append("; "); - log.append(name); - log.append(": "); - boolean first = true; - for (Base b : contents) { - if (first) - first = false; + private List execute(ExecutionContext context, List focus, ExpressionNode exp, boolean atEntry) throws FHIRException { +// System.out.println("Evaluate {'"+exp.toString()+"'} on "+focus.toString()); + List work = new ArrayList(); + switch (exp.getKind()) { + case Name: + if (atEntry && exp.getName().equals("$this")) + work.add(context.getThisItem()); else - log.append(","); - log.append(convertToString(b)); + for (Base item : focus) { + List outcome = execute(context, item, exp, atEntry); + for (Base base : outcome) + if (base != null) + work.add(base); + } + break; + case Function: + List work2 = evaluateFunction(context, focus, exp); + work.addAll(work2); + break; + case Constant: + Base b = processConstant(context, exp.getConstant()); + if (b != null) + work.add(b); + break; + case Group: + work2 = execute(context, focus, exp.getGroup(), atEntry); + work.addAll(work2); + } + + if (exp.getInner() != null) + work = execute(context, work, exp.getInner(), false); + + if (exp.isProximal() && exp.getOperation() != null) { + ExpressionNode next = exp.getOpNext(); + ExpressionNode last = exp; + while (next != null) { + List work2 = preOperate(work, last.getOperation()); + if (work2 != null) + work = work2; + else if (last.getOperation() == Operation.Is || last.getOperation() == Operation.As) { + work2 = executeTypeName(context, focus, next, false); + work = operate(work, last.getOperation(), work2); + } else { + work2 = execute(context, focus, next, true); + work = operate(work, last.getOperation(), work2); +// System.out.println("Result of {'"+last.toString()+" "+last.getOperation().toCode()+" "+next.toString()+"'}: "+focus.toString()); + } + last = next; + next = next.getOpNext(); } } +// System.out.println("Result of {'"+exp.toString()+"'}: "+work.toString()); + return work; } - public String forLog() { - if (log.length() > 0) - return " ("+log.toString()+")"; - else - return ""; - } - - private class ExecutionContext { - private Object appInfo; - private Base resource; - private Base context; - private Base thisItem; - private Map aliases; - - public ExecutionContext(Object appInfo, Base resource, Base context, Map aliases, Base thisItem) { - this.appInfo = appInfo; - this.context = context; - this.resource = resource; - this.aliases = aliases; - this.thisItem = thisItem; - } - public Base getResource() { - return resource; - } - public Base getThisItem() { - return thisItem; - } - public void addAlias(String name, List focus) throws FHIRException { - if (aliases == null) - aliases = new HashMap(); - else - aliases = new HashMap(aliases); // clone it, since it's going to change - if (focus.size() > 1) - throw new FHIRException("Attempt to alias a collection, not a singleton"); - aliases.put(name, focus.size() == 0 ? null : focus.get(0)); - } - public Base getAlias(String name) { - return aliases == null ? null : aliases.get(name); - } - } - - private class ExecutionTypeContext { - private Object appInfo; - private String resource; - private String context; - private TypeDetails thisItem; - - - public ExecutionTypeContext(Object appInfo, String resource, String context, TypeDetails thisItem) { - super(); - this.appInfo = appInfo; - this.resource = resource; - this.context = context; - this.thisItem = thisItem; - - } - public String getResource() { - return resource; - } - public TypeDetails getThisItem() { - return thisItem; - } - } - - private ExpressionNode parseExpression(FHIRLexer lexer, boolean proximal) throws FHIRLexerException { - ExpressionNode result = new ExpressionNode(lexer.nextId()); - SourceLocation c = lexer.getCurrentStartLocation(); - result.setStart(lexer.getCurrentLocation()); - // special: - if (lexer.getCurrent().equals("-")) { - lexer.take(); - lexer.setCurrent("-"+lexer.getCurrent()); - } - if (lexer.getCurrent().equals("+")) { - lexer.take(); - lexer.setCurrent("+"+lexer.getCurrent()); - } - if (lexer.isConstant(false)) { - checkConstant(lexer.getCurrent(), lexer); - result.setConstant(lexer.take()); - result.setKind(Kind.Constant); - result.setEnd(lexer.getCurrentLocation()); - } else if ("(".equals(lexer.getCurrent())) { - lexer.next(); - result.setKind(Kind.Group); - result.setGroup(parseExpression(lexer, true)); - if (!")".equals(lexer.getCurrent())) - throw lexer.error("Found "+lexer.getCurrent()+" expecting a \")\""); - result.setEnd(lexer.getCurrentLocation()); - lexer.next(); - } else { - if (!lexer.isToken() && !lexer.getCurrent().startsWith("\"")) - throw lexer.error("Found "+lexer.getCurrent()+" expecting a token name"); - if (lexer.getCurrent().startsWith("\"")) - result.setName(lexer.readConstant("Path Name")); - else - result.setName(lexer.take()); - result.setEnd(lexer.getCurrentLocation()); - if (!result.checkName()) - throw lexer.error("Found "+result.getName()+" expecting a valid token name"); - if ("(".equals(lexer.getCurrent())) { - Function f = Function.fromCode(result.getName()); - FunctionDetails details = null; - if (f == null) { - if (hostServices != null) - details = hostServices.resolveFunction(result.getName()); - if (details == null) - throw lexer.error("The name "+result.getName()+" is not a valid function name"); - f = Function.Custom; - } - result.setKind(Kind.Function); - result.setFunction(f); - lexer.next(); - while (!")".equals(lexer.getCurrent())) { - result.getParameters().add(parseExpression(lexer, true)); - if (",".equals(lexer.getCurrent())) - lexer.next(); - else if (!")".equals(lexer.getCurrent())) - throw lexer.error("The token "+lexer.getCurrent()+" is not expected here - either a \",\" or a \")\" expected"); - } - result.setEnd(lexer.getCurrentLocation()); - lexer.next(); - checkParameters(lexer, c, result, details); - } else - result.setKind(Kind.Name); - } - ExpressionNode focus = result; - if ("[".equals(lexer.getCurrent())) { - lexer.next(); - ExpressionNode item = new ExpressionNode(lexer.nextId()); - item.setKind(Kind.Function); - item.setFunction(ExpressionNode.Function.Item); - item.getParameters().add(parseExpression(lexer, true)); - if (!lexer.getCurrent().equals("]")) - throw lexer.error("The token "+lexer.getCurrent()+" is not expected here - a \"]\" expected"); - lexer.next(); - result.setInner(item); - focus = item; - } - if (".".equals(lexer.getCurrent())) { - lexer.next(); - focus.setInner(parseExpression(lexer, false)); - } - result.setProximal(proximal); - if (proximal) { - while (lexer.isOp()) { - focus.setOperation(ExpressionNode.Operation.fromCode(lexer.getCurrent())); - focus.setOpStart(lexer.getCurrentStartLocation()); - focus.setOpEnd(lexer.getCurrentLocation()); - lexer.next(); - focus.setOpNext(parseExpression(lexer, false)); - focus = focus.getOpNext(); + private List execute(ExecutionContext context, Base item, ExpressionNode exp, boolean atEntry) throws FHIRException { + List result = new ArrayList(); + if (atEntry && Character.isUpperCase(exp.getName().charAt(0))) {// special case for start up + if (item.isResource() && item.fhirType().equals(exp.getName())) + result.add(item); + } else + getChildrenByName(item, exp.getName(), result); + if (result.size() == 0 && atEntry && context.appInfo != null) { + Base temp = hostServices.resolveConstant(context.appInfo, exp.getName()); + if (temp != null) { + result.add(temp); } - result = organisePrecedence(lexer, result); } return result; } - private ExpressionNode organisePrecedence(FHIRLexer lexer, ExpressionNode node) { - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Times, Operation.DivideBy, Operation.Div, Operation.Mod)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Plus, Operation.Minus, Operation.Concatenate)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Union)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.LessThen, Operation.Greater, Operation.LessOrEqual, Operation.GreaterOrEqual)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Is)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Equals, Operation.Equivalent, Operation.NotEquals, Operation.NotEquivalent)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.And)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Xor, Operation.Or)); - // last: implies - return node; + private TypeDetails executeContextType(ExecutionTypeContext context, String name) throws PathEngineException, DefinitionException { + if (hostServices == null) + throw new PathEngineException("Unable to resolve context reference since no host services are provided"); + return hostServices.resolveConstantType(context.appInfo, name); + } + + private TypeDetails executeType(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { + TypeDetails result = new TypeDetails(null); + switch (exp.getKind()) { + case Name: + if (atEntry && exp.getName().equals("$this")) + result.update(context.getThisItem()); + else if (atEntry && focus == null) + result.update(executeContextType(context, exp.getName())); + else { + for (String s : focus.getTypes()) { + result.update(executeType(s, exp, atEntry)); + } + if (result.hasNoTypes()) + throw new PathEngineException("The name " + exp.getName() + " is not valid for any of the possible types: " + focus.describe()); + } + break; + case Function: + result.update(evaluateFunctionType(context, focus, exp)); + break; + case Constant: + result.update(readConstantType(context, exp.getConstant())); + break; + case Group: + result.update(executeType(context, focus, exp.getGroup(), atEntry)); + } + exp.setTypes(result); + + if (exp.getInner() != null) { + result = executeType(context, result, exp.getInner(), false); + } + + if (exp.isProximal() && exp.getOperation() != null) { + ExpressionNode next = exp.getOpNext(); + ExpressionNode last = exp; + while (next != null) { + TypeDetails work; + if (last.getOperation() == Operation.Is || last.getOperation() == Operation.As) + work = executeTypeName(context, focus, next, atEntry); + else + work = executeType(context, focus, next, atEntry); + result = operateTypes(result, last.getOperation(), work); + last = next; + next = next.getOpNext(); + } + exp.setOpTypes(result); + } + return result; + } + + private TypeDetails executeType(String type, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { + if (atEntry && Character.isUpperCase(exp.getName().charAt(0)) && tail(type).equals(exp.getName())) // special case for start up + return new TypeDetails(CollectionStatus.SINGLETON, type); + TypeDetails result = new TypeDetails(null); + getChildTypesByName(type, exp.getName(), result); + return result; + } + + private List executeTypeName(ExecutionContext context, List focus, ExpressionNode next, boolean atEntry) { + List result = new ArrayList(); + result.add(new StringType(next.getName())); + return result; + } + + private TypeDetails executeTypeName(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { + return new TypeDetails(CollectionStatus.SINGLETON, exp.getName()); + } + + public String forLog() { + if (log.length() > 0) + return " (" + log.toString() + ")"; + else + return ""; + } + + private List funcAlias(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List nl = execute(context, focus, exp.getParameters().get(0), true); + String name = nl.get(0).primitiveValue(); + List res = new ArrayList(); + Base b = context.getAlias(name); + if (b != null) + res.add(b); + return res; + + } + + private List funcAliasAs(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List nl = execute(context, focus, exp.getParameters().get(0), true); + String name = nl.get(0).primitiveValue(); + context.addAlias(name, focus); + return focus; + } + + private List funcAll(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + if (exp.getParameters().size() == 1) { + List result = new ArrayList(); + List pc = new ArrayList(); + boolean all = true; + for (Base item : focus) { + pc.clear(); + pc.add(item); + if (!convertToBoolean(execute(changeThis(context, item), pc, exp.getParameters().get(0), true))) { + all = false; + break; + } + } + result.add(new BooleanType(all)); + return result; + } else {// (exp.getParameters().size() == 0) { + List result = new ArrayList(); + boolean all = true; + for (Base item : focus) { + boolean v = false; + if (item instanceof BooleanType) { + v = ((BooleanType) item).booleanValue(); + } else + v = item != null; + if (!v) { + all = false; + break; + } + } + result.add(new BooleanType(all)); + return result; + } + } + + private List funcAs(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + String tn = exp.getParameters().get(0).getName(); + for (Base b : focus) + if (b.hasType(tn)) + result.add(b); + return result; + } + + private List funcChildren(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + for (Base b : focus) + getChildrenByName(b, "*", result); + return result; + } + + private List funcContains(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) { + String st = convertToString(focus.get(0)); + if (Utilities.noString(st)) + result.add(new BooleanType(false)); + else + result.add(new BooleanType(st.contains(sw))); + } else + result.add(new BooleanType(false)); + return result; + } + + private List funcCount(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new IntegerType(focus.size())); + return result; + } + + private List funcDescendants(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List current = new ArrayList(); + current.addAll(focus); + List added = new ArrayList(); + boolean more = true; + while (more) { + added.clear(); + for (Base item : current) { + getChildrenByName(item, "*", added); + } + more = !added.isEmpty(); + result.addAll(added); + current.clear(); + current.addAll(added); + } + return result; + } + + private List funcDistinct(ExecutionContext context, List focus, ExpressionNode exp) { + if (focus.size() <= 1) + return focus; + + List result = new ArrayList(); + for (int i = 0; i < focus.size(); i++) { + boolean found = false; + for (int j = i + 1; j < focus.size(); j++) { + if (doEquals(focus.get(j), focus.get(i))) { + found = true; + break; + } + } + if (!found) + result.add(focus.get(i)); + } + return result; + } + + private List funcEmpty(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new BooleanType(ElementUtil.isEmpty(focus))); + return result; + } + + private List funcEndsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) + result.add(new BooleanType(convertToString(focus.get(0)).endsWith(sw))); + else + result.add(new BooleanType(false)); + return result; + } + + private List funcExists(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new BooleanType(!ElementUtil.isEmpty(focus))); + return result; + } + + private List funcExtension(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List nl = execute(context, focus, exp.getParameters().get(0), true); + String url = nl.get(0).primitiveValue(); + + for (Base item : focus) { + List ext = new ArrayList(); + getChildrenByName(item, "extension", ext); + getChildrenByName(item, "modifierExtension", ext); + for (Base ex : ext) { + List vl = new ArrayList(); + getChildrenByName(ex, "url", vl); + if (convertToString(vl).equals(url)) + result.add(ex); + } + } + return result; + } + + private List funcFirst(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() > 0) + result.add(focus.get(0)); + return result; + } + + private List funcHasValue(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() == 1) { + String s = convertToString(focus.get(0)); + result.add(new BooleanType(!Utilities.noString(s))); + } + return result; + } + + private List funcIif(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List n1 = execute(context, focus, exp.getParameters().get(0), true); + Boolean v = convertToBoolean(n1); + + if (v) + return execute(context, focus, exp.getParameters().get(1), true); + else if (exp.getParameters().size() < 3) + return new ArrayList(); + else + return execute(context, focus, exp.getParameters().get(2), true); + } + + private List funcIs(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { + List result = new ArrayList(); + if (focus.size() == 0 || focus.size() > 1) + result.add(new BooleanType(false)); + else { + String tn = exp.getParameters().get(0).getName(); + result.add(new BooleanType(focus.get(0).hasType(tn))); + } + return result; + } + + private List funcIsDistinct(ExecutionContext context, List focus, ExpressionNode exp) { + if (focus.size() <= 1) + return makeBoolean(true); + + boolean distinct = true; + for (int i = 0; i < focus.size(); i++) { + for (int j = i + 1; j < focus.size(); j++) { + if (doEquals(focus.get(j), focus.get(i))) { + distinct = false; + break; + } + } + } + return makeBoolean(distinct); + } + + private List funcItem(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String s = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + if (Utilities.isInteger(s) && Integer.parseInt(s) < focus.size()) + result.add(focus.get(Integer.parseInt(s))); + return result; + } + + private List funcLast(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() > 0) + result.add(focus.get(focus.size() - 1)); + return result; + } + + private List funcLength(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() == 1) { + String s = convertToString(focus.get(0)); + result.add(new IntegerType(s.length())); + } + return result; + } + + private List funcMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) { + String st = convertToString(focus.get(0)); + if (Utilities.noString(st)) + result.add(new BooleanType(false)); + else + result.add(new BooleanType(st.matches(sw))); + } else + result.add(new BooleanType(false)); + return result; + } + + private List funcMemberOf(ExecutionContext context, List focus, ExpressionNode exp) { + throw new Error("not Implemented yet"); + } + + private List funcNot(ExecutionContext context, List focus, ExpressionNode exp) { + return makeBoolean(!convertToBoolean(focus)); + } + + private List funcNow(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(DateTimeType.now()); + return result; + } + + private List funcRepeat(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List current = new ArrayList(); + current.addAll(focus); + List added = new ArrayList(); + boolean more = true; + while (more) { + added.clear(); + List pc = new ArrayList(); + for (Base item : current) { + pc.clear(); + pc.add(item); + added.addAll(execute(changeThis(context, item), pc, exp.getParameters().get(0), false)); + } + more = !added.isEmpty(); + result.addAll(added); + current.clear(); + current.addAll(added); + } + return result; + } + + private List funcReplace(ExecutionContext context, List focus, ExpressionNode exp) { + throw new Error("not Implemented yet"); + } + + private List funcReplaceMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) + result.add(new BooleanType(convertToString(focus.get(0)).contains(sw))); + else + result.add(new BooleanType(false)); + return result; + } + + private List funcResolve(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + for (Base item : focus) { + String s = convertToString(item); + if (item.fhirType().equals("Reference")) { + Property p = item.getChildByName("reference"); + if (p.hasValues()) + s = convertToString(p.getValues().get(0)); + } + Base res = null; + if (s.startsWith("#")) { + Property p = context.resource.getChildByName("contained"); + for (Base c : p.getValues()) { + if (s.equals(c.getIdBase())) + res = c; + } + } else if (hostServices != null) { + res = hostServices.resolveReference(context.appInfo, s); + } + if (res != null) + result.add(res); + } + return result; + } + + private List funcSelect(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List pc = new ArrayList(); + for (Base item : focus) { + pc.clear(); + pc.add(item); + result.addAll(execute(changeThis(context, item), pc, exp.getParameters().get(0), true)); + } + return result; + } + + private List funcSingle(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { + if (focus.size() == 1) + return focus; + throw new PathEngineException(String.format("Single() : checking for 1 item but found %d items", focus.size())); + } + + private List funcSkip(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List n1 = execute(context, focus, exp.getParameters().get(0), true); + int i1 = Integer.parseInt(n1.get(0).primitiveValue()); + + List result = new ArrayList(); + for (int i = i1; i < focus.size(); i++) + result.add(focus.get(i)); + return result; + } + + private List funcStartsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) + result.add(new BooleanType(convertToString(focus.get(0)).startsWith(sw))); + else + result.add(new BooleanType(false)); + return result; + } + + private List funcSubsetOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List target = execute(context, focus, exp.getParameters().get(0), true); + + boolean valid = true; + for (Base item : focus) { + boolean found = false; + for (Base t : target) { + if (Base.compareDeep(item, t, false)) { + found = true; + break; + } + } + if (!found) { + valid = false; + break; + } + } + List result = new ArrayList(); + result.add(new BooleanType(valid)); + return result; + } + + private List funcSubstring(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List n1 = execute(context, focus, exp.getParameters().get(0), true); + int i1 = Integer.parseInt(n1.get(0).primitiveValue()); + int i2 = -1; + if (exp.parameterCount() == 2) { + List n2 = execute(context, focus, exp.getParameters().get(1), true); + i2 = Integer.parseInt(n2.get(0).primitiveValue()); + } + + if (focus.size() == 1) { + String sw = convertToString(focus.get(0)); + String s; + if (i1 < 0 || i1 >= sw.length()) + return new ArrayList(); + if (exp.parameterCount() == 2) + s = sw.substring(i1, Math.min(sw.length(), i1 + i2)); + else + s = sw.substring(i1); + if (!Utilities.noString(s)) + result.add(new StringType(s)); + } + return result; + } + + private List funcSupersetOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List target = execute(context, focus, exp.getParameters().get(0), true); + + boolean valid = true; + for (Base item : target) { + boolean found = false; + for (Base t : focus) { + if (Base.compareDeep(item, t, false)) { + found = true; + break; + } + } + if (!found) { + valid = false; + break; + } + } + List result = new ArrayList(); + result.add(new BooleanType(valid)); + return result; + } + + private List funcTail(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + for (int i = 1; i < focus.size(); i++) + result.add(focus.get(i)); + return result; + } + + private List funcTake(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List n1 = execute(context, focus, exp.getParameters().get(0), true); + int i1 = Integer.parseInt(n1.get(0).primitiveValue()); + + List result = new ArrayList(); + for (int i = 0; i < Math.min(focus.size(), i1); i++) + result.add(focus.get(i)); + return result; + } + + private List funcToDecimal(ExecutionContext context, List focus, ExpressionNode exp) { + String s = convertToString(focus); + List result = new ArrayList(); + if (Utilities.isDecimal(s)) + result.add(new DecimalType(s)); + return result; + } + + private List funcToInteger(ExecutionContext context, List focus, ExpressionNode exp) { + String s = convertToString(focus); + List result = new ArrayList(); + if (Utilities.isInteger(s)) + result.add(new IntegerType(s)); + return result; + } + + private List funcToString(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new StringType(convertToString(focus))); + return result; + } + + private List funcToday(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new DateType(new Date(), TemporalPrecisionEnum.DAY)); + return result; + } + + private List funcTrace(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List nl = execute(context, focus, exp.getParameters().get(0), true); + String name = nl.get(0).primitiveValue(); + + log(name, focus); + return focus; + } + + private List funcWhere(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List pc = new ArrayList(); + for (Base item : focus) { + pc.clear(); + pc.add(item); + if (convertToBoolean(execute(changeThis(context, item), pc, exp.getParameters().get(0), true))) + result.add(item); + } + return result; } private ExpressionNode gatherPrecedence(FHIRLexer lexer, ExpressionNode start, EnumSet ops) { // work : boolean; // focus, node, group : ExpressionNode; - assert(start.isProximal()); + assert (start.isProximal()); // is there anything to do? boolean work = false; @@ -740,186 +1590,230 @@ public class FHIRPathEngine { return start; } + private void getChildTypesByName(String type, String name, TypeDetails result) throws PathEngineException, DefinitionException { + if (Utilities.noString(type)) + throw new PathEngineException("No type provided in BuildToolPathEvaluator.getChildTypesByName"); + if (type.equals("http://hl7.org/fhir/StructureDefinition/xhtml")) + return; + String url = null; + if (type.contains("#")) { + url = type.substring(0, type.indexOf("#")); + } else { + url = type; + } + String tail = ""; + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, url); + if (sd == null) + throw new DefinitionException("Unknown type " + type); // this really is an error, because we can only get to here if the internal infrastrucgture is wrong + List sdl = new ArrayList(); + ElementDefinitionMatch m = null; + if (type.contains("#")) + m = getElementDefinition(sd, type.substring(type.indexOf("#") + 1), false); + if (m != null && hasDataType(m.definition)) { + if (m.fixedType != null) { + StructureDefinition dt = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + m.fixedType); + if (dt == null) + throw new DefinitionException("unknown data type " + m.fixedType); + sdl.add(dt); + } else + for (TypeRefComponent t : m.definition.getType()) { + StructureDefinition dt = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + t.getCode()); + if (dt == null) + throw new DefinitionException("unknown data type " + t.getCode()); + sdl.add(dt); + } + } else { + sdl.add(sd); + if (type.contains("#")) { + tail = type.substring(type.indexOf("#") + 1); + tail = tail.substring(tail.indexOf(".")); + } + } - private ExpressionNode newGroup(FHIRLexer lexer, ExpressionNode next) { - ExpressionNode result = new ExpressionNode(lexer.nextId()); - result.setKind(Kind.Group); - result.setGroup(next); - result.getGroup().setProximal(true); - return result; - } + for (StructureDefinition sdi : sdl) { + String path = sdi.getSnapshot().getElement().get(0).getPath() + tail + "."; + if (name.equals("**")) { + assert (result.getCollectionStatus() == CollectionStatus.UNORDERED); + for (ElementDefinition ed : sdi.getSnapshot().getElement()) { + if (ed.getPath().startsWith(path)) + for (TypeRefComponent t : ed.getType()) { + if (t.hasCode() && t.getCodeElement().hasValue()) { + String tn = null; + if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) + tn = sdi.getType() + "#" + ed.getPath(); + else + tn = t.getCode(); + if (t.getCode().equals("Resource")) { + for (String rn : worker.getResourceNames()) { + if (!result.hasType(worker, rn)) { + getChildTypesByName(result.addType(rn), "**", result); + } + } + } else if (!result.hasType(worker, tn)) { + getChildTypesByName(result.addType(tn), "**", result); + } + } + } + } + } else if (name.equals("*")) { + assert (result.getCollectionStatus() == CollectionStatus.UNORDERED); + for (ElementDefinition ed : sdi.getSnapshot().getElement()) { + if (ed.getPath().startsWith(path) && !ed.getPath().substring(path.length()).contains(".")) + for (TypeRefComponent t : ed.getType()) { + if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) + result.addType(sdi.getType() + "#" + ed.getPath()); + else if (t.getCode().equals("Resource")) + result.addTypes(worker.getResourceNames()); + else + result.addType(t.getCode()); + } + } + } else { + path = sdi.getSnapshot().getElement().get(0).getPath() + tail + "." + name; - private void checkConstant(String s, FHIRLexer lexer) throws FHIRLexerException { - if (s.startsWith("\'") && s.endsWith("\'")) { - int i = 1; - while (i < s.length()-1) { - char ch = s.charAt(i); - if (ch == '\\') { - switch (ch) { - case 't': - case 'r': - case 'n': - case 'f': - case '\'': - case '\\': - case '/': - i++; - break; - case 'u': - if (!Utilities.isHex("0x"+s.substring(i, i+4))) - throw lexer.error("Improper unicode escape \\u"+s.substring(i, i+4)); - break; - default: - throw lexer.error("Unknown character escape \\"+ch); - } - } else - i++; + ElementDefinitionMatch ed = getElementDefinition(sdi, path, false); + if (ed != null) { + if (!Utilities.noString(ed.getFixedType())) + result.addType(ed.getFixedType()); + else + for (TypeRefComponent t : ed.getDefinition().getType()) { + if (Utilities.noString(t.getCode())) + break; // throw new PathEngineException("Illegal reference to primitive value attribute @ "+path); + + ProfiledType pt = null; + if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) + pt = new ProfiledType(sdi.getUrl() + "#" + path); + else if (t.getCode().equals("Resource")) + result.addTypes(worker.getResourceNames()); + else + pt = new ProfiledType(t.getCode()); + if (pt != null) { + if (t.hasProfile()) + pt.addProfile(t.getProfile()); + if (ed.getDefinition().hasBinding()) + pt.addBinding(ed.getDefinition().getBinding()); + result.addType(pt); + } + } + } } } } - // procedure CheckParamCount(c : integer); - // begin - // if exp.Parameters.Count <> c then - // raise lexer.error('The function "'+exp.name+'" requires '+inttostr(c)+' parameters', offset); - // end; + // private boolean isPrimitiveType(String s) { + // return s.equals("boolean") || s.equals("integer") || s.equals("decimal") || s.equals("base64Binary") || s.equals("instant") || s.equals("string") || s.equals("uri") || s.equals("date") || s.equals("dateTime") || s.equals("time") || s.equals("code") || s.equals("oid") || s.equals("id") || s.equals("unsignedInt") || s.equals("positiveInt") || s.equals("markdown"); + // } - private boolean checkParamCount(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, int count) throws FHIRLexerException { - if (exp.getParameters().size() != count) - throw lexer.error("The function \""+exp.getName()+"\" requires "+Integer.toString(count)+" parameters", location.toString()); - return true; + /** + * Given an item, return all the children that conform to the pattern described in name + *

    + * Possible patterns: + * - a simple name (which may be the base of a name with [] e.g. value[x]) + * - a name with a type replacement e.g. valueCodeableConcept + * - * which means all children + * - ** which means all descendants + * + * @param item + * @param name + * @param result + * @throws FHIRException + */ + protected void getChildrenByName(Base item, String name, List result) throws FHIRException { + Base[] list = item.listChildrenByName(name, false); + if (list != null) + for (Base v : list) + if (v != null) + result.add(v); } - private boolean checkParamCount(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, int countMin, int countMax) throws FHIRLexerException { - if (exp.getParameters().size() < countMin || exp.getParameters().size() > countMax) - throw lexer.error("The function \""+exp.getName()+"\" requires between "+Integer.toString(countMin)+" and "+Integer.toString(countMax)+" parameters", location.toString()); - return true; - } - - private boolean checkParameters(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, FunctionDetails details) throws FHIRLexerException { - switch (exp.getFunction()) { - case Empty: return checkParamCount(lexer, location, exp, 0); - case Not: return checkParamCount(lexer, location, exp, 0); - case Exists: return checkParamCount(lexer, location, exp, 0); - case SubsetOf: return checkParamCount(lexer, location, exp, 1); - case SupersetOf: return checkParamCount(lexer, location, exp, 1); - case IsDistinct: return checkParamCount(lexer, location, exp, 0); - case Distinct: return checkParamCount(lexer, location, exp, 0); - case Count: return checkParamCount(lexer, location, exp, 0); - case Where: return checkParamCount(lexer, location, exp, 1); - case Select: return checkParamCount(lexer, location, exp, 1); - case All: return checkParamCount(lexer, location, exp, 0, 1); - case Repeat: return checkParamCount(lexer, location, exp, 1); - case Item: return checkParamCount(lexer, location, exp, 1); - case As: return checkParamCount(lexer, location, exp, 1); - case Is: return checkParamCount(lexer, location, exp, 1); - case Single: return checkParamCount(lexer, location, exp, 0); - case First: return checkParamCount(lexer, location, exp, 0); - case Last: return checkParamCount(lexer, location, exp, 0); - case Tail: return checkParamCount(lexer, location, exp, 0); - case Skip: return checkParamCount(lexer, location, exp, 1); - case Take: return checkParamCount(lexer, location, exp, 1); - case Iif: return checkParamCount(lexer, location, exp, 2,3); - case ToInteger: return checkParamCount(lexer, location, exp, 0); - case ToDecimal: return checkParamCount(lexer, location, exp, 0); - case ToString: return checkParamCount(lexer, location, exp, 0); - case Substring: return checkParamCount(lexer, location, exp, 1, 2); - case StartsWith: return checkParamCount(lexer, location, exp, 1); - case EndsWith: return checkParamCount(lexer, location, exp, 1); - case Matches: return checkParamCount(lexer, location, exp, 1); - case ReplaceMatches: return checkParamCount(lexer, location, exp, 2); - case Contains: return checkParamCount(lexer, location, exp, 1); - case Replace: return checkParamCount(lexer, location, exp, 2); - case Length: return checkParamCount(lexer, location, exp, 0); - case Children: return checkParamCount(lexer, location, exp, 0); - case Descendants: return checkParamCount(lexer, location, exp, 0); - case MemberOf: return checkParamCount(lexer, location, exp, 1); - case Trace: return checkParamCount(lexer, location, exp, 1); - case Today: return checkParamCount(lexer, location, exp, 0); - case Now: return checkParamCount(lexer, location, exp, 0); - case Resolve: return checkParamCount(lexer, location, exp, 0); - case Extension: return checkParamCount(lexer, location, exp, 1); - case HasValue: return checkParamCount(lexer, location, exp, 0); - case Alias: return checkParamCount(lexer, location, exp, 1); - case AliasAs: return checkParamCount(lexer, location, exp, 1); - case Custom: return checkParamCount(lexer, location, exp, details.getMinParameters(), details.getMaxParameters()); + private ElementDefinitionMatch getElementDefinition(StructureDefinition sd, String path, boolean allowTypedName) throws PathEngineException { + for (ElementDefinition ed : sd.getSnapshot().getElement()) { + if (ed.getPath().equals(path)) { + if (ed.hasContentReference()) { + return getElementDefinitionById(sd, ed.getContentReference()); + } else + return new ElementDefinitionMatch(ed, null); + } + if (ed.getPath().endsWith("[x]") && path.startsWith(ed.getPath().substring(0, ed.getPath().length() - 3)) && path.length() == ed.getPath().length() - 3) + return new ElementDefinitionMatch(ed, null); + if (allowTypedName && ed.getPath().endsWith("[x]") && path.startsWith(ed.getPath().substring(0, ed.getPath().length() - 3)) && path.length() > ed.getPath().length() - 3) { + String s = Utilities.uncapitalize(path.substring(ed.getPath().length() - 3)); + if (primitiveTypes.contains(s)) + return new ElementDefinitionMatch(ed, s); + else + return new ElementDefinitionMatch(ed, path.substring(ed.getPath().length() - 3)); + } + if (ed.getPath().contains(".") && path.startsWith(ed.getPath() + ".") && (ed.getType().size() > 0) && !isAbstractType(ed.getType())) { + // now we walk into the type. + if (ed.getType().size() > 1) // if there's more than one type, the test above would fail this + throw new PathEngineException("Internal typing issue...."); + StructureDefinition nsd = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + ed.getType().get(0).getCode()); + if (nsd == null) + throw new PathEngineException("Unknown type " + ed.getType().get(0).getCode()); + return getElementDefinition(nsd, nsd.getId() + path.substring(ed.getPath().length()), allowTypedName); + } + if (ed.hasContentReference() && path.startsWith(ed.getPath() + ".")) { + ElementDefinitionMatch m = getElementDefinitionById(sd, ed.getContentReference()); + return getElementDefinition(sd, m.definition.getPath() + path.substring(ed.getPath().length()), allowTypedName); + } } + return null; + } + + private ElementDefinitionMatch getElementDefinitionById(StructureDefinition sd, String ref) { + for (ElementDefinition ed : sd.getSnapshot().getElement()) { + if (ref.equals("#" + ed.getId())) + return new ElementDefinitionMatch(ed, null); + } + return null; + } + + public IEvaluationContext getHostServices() { + return hostServices; + } + + public void setHostServices(IEvaluationContext constantResolver) { + this.hostServices = constantResolver; + } + + private boolean hasDataType(ElementDefinition ed) { + return ed.hasType() && !(ed.getType().get(0).getCode().equals("Element") || ed.getType().get(0).getCode().equals("BackboneElement")); + } + + public boolean hasLog() { + return log != null && log.length() > 0; + } + + private boolean hasType(ElementDefinition ed, String s) { + for (TypeRefComponent t : ed.getType()) + if (s.equalsIgnoreCase(t.getCode())) + return true; return false; } - private List execute(ExecutionContext context, List focus, ExpressionNode exp, boolean atEntry) throws FHIRException { -// System.out.println("Evaluate {'"+exp.toString()+"'} on "+focus.toString()); - List work = new ArrayList(); - switch (exp.getKind()) { - case Name: - if (atEntry && exp.getName().equals("$this")) - work.add(context.getThisItem()); - else - for (Base item : focus) { - List outcome = execute(context, item, exp, atEntry); - for (Base base : outcome) - if (base != null) - work.add(base); - } - break; - case Function: - List work2 = evaluateFunction(context, focus, exp); - work.addAll(work2); - break; - case Constant: - Base b = processConstant(context, exp.getConstant()); - if (b != null) - work.add(b); - break; - case Group: - work2 = execute(context, focus, exp.getGroup(), atEntry); - work.addAll(work2); - } + private boolean isAbstractType(List list) { + return list.size() != 1 ? true : Utilities.existsInList(list.get(0).getCode(), "Element", "BackboneElement", "Resource", "DomainResource"); + } - if (exp.getInner() != null) - work = execute(context, work, exp.getInner(), false); + private boolean isBoolean(List list, boolean b) { + return list.size() == 1 && list.get(0) instanceof BooleanType && ((BooleanType) list.get(0)).booleanValue() == b; + } - if (exp.isProximal() && exp.getOperation() != null) { - ExpressionNode next = exp.getOpNext(); - ExpressionNode last = exp; - while (next != null) { - List work2 = preOperate(work, last.getOperation()); - if (work2 != null) - work = work2; - else if (last.getOperation() == Operation.Is || last.getOperation() == Operation.As) { - work2 = executeTypeName(context, focus, next, false); - work = operate(work, last.getOperation(), work2); - } else { - work2 = execute(context, focus, next, true); - work = operate(work, last.getOperation(), work2); -// System.out.println("Result of {'"+last.toString()+" "+last.getOperation().toCode()+" "+next.toString()+"'}: "+focus.toString()); - } - last = next; - next = next.getOpNext(); + private void log(String name, List contents) { + if (hostServices == null || !hostServices.log(name, contents)) { + if (log.length() > 0) + log.append("; "); + log.append(name); + log.append(": "); + boolean first = true; + for (Base b : contents) { + if (first) + first = false; + else + log.append(","); + log.append(convertToString(b)); } } -// System.out.println("Result of {'"+exp.toString()+"'}: "+work.toString()); - return work; - } - - private List executeTypeName(ExecutionContext context, List focus, ExpressionNode next, boolean atEntry) { - List result = new ArrayList(); - result.add(new StringType(next.getName())); - return result; - } - - - private List preOperate(List left, Operation operation) { - switch (operation) { - case And: - return isBoolean(left, false) ? makeBoolean(false) : null; - case Or: - return isBoolean(left, true) ? makeBoolean(true) : null; - case Implies: - return convertToBoolean(left) ? null : makeBoolean(true); - default: - return null; - } } private List makeBoolean(boolean b) { @@ -928,205 +1822,25 @@ public class FHIRPathEngine { return res; } - private TypeDetails executeTypeName(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { - return new TypeDetails(CollectionStatus.SINGLETON, exp.getName()); - } - - private TypeDetails executeType(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { - TypeDetails result = new TypeDetails(null); - switch (exp.getKind()) { - case Name: - if (atEntry && exp.getName().equals("$this")) - result.update(context.getThisItem()); - else if (atEntry && focus == null) - result.update(executeContextType(context, exp.getName())); - else { - for (String s : focus.getTypes()) { - result.update(executeType(s, exp, atEntry)); - } - if (result.hasNoTypes()) - throw new PathEngineException("The name "+exp.getName()+" is not valid for any of the possible types: "+focus.describe()); - } - break; - case Function: - result.update(evaluateFunctionType(context, focus, exp)); - break; - case Constant: - result.update(readConstantType(context, exp.getConstant())); - break; - case Group: - result.update(executeType(context, focus, exp.getGroup(), atEntry)); - } - exp.setTypes(result); - - if (exp.getInner() != null) { - result = executeType(context, result, exp.getInner(), false); - } - - if (exp.isProximal() && exp.getOperation() != null) { - ExpressionNode next = exp.getOpNext(); - ExpressionNode last = exp; - while (next != null) { - TypeDetails work; - if (last.getOperation() == Operation.Is || last.getOperation() == Operation.As) - work = executeTypeName(context, focus, next, atEntry); - else - work = executeType(context, focus, next, atEntry); - result = operateTypes(result, last.getOperation(), work); - last = next; - next = next.getOpNext(); - } - exp.setOpTypes(result); - } + private ExpressionNode newGroup(FHIRLexer lexer, ExpressionNode next) { + ExpressionNode result = new ExpressionNode(lexer.nextId()); + result.setKind(Kind.Group); + result.setGroup(next); + result.getGroup().setProximal(true); return result; } - private Base processConstant(ExecutionContext context, String constant) throws PathEngineException { - if (constant.equals("true")) { - return new BooleanType(true); - } else if (constant.equals("false")) { - return new BooleanType(false); - } else if (constant.equals("{}")) { - return null; - } else if (Utilities.isInteger(constant)) { - return new IntegerType(constant); - } else if (Utilities.isDecimal(constant)) { - return new DecimalType(constant); - } else if (constant.startsWith("\'")) { - return new StringType(processConstantString(constant)); - } else if (constant.startsWith("%")) { - return resolveConstant(context, constant); - } else if (constant.startsWith("@")) { - return processDateConstant(context.appInfo, constant.substring(1)); - } else { - return new StringType(constant); - } - } - - private Base processDateConstant(Object appInfo, String value) throws PathEngineException { - if (value.startsWith("T")) - return new TimeType(value.substring(1)); - String v = value; - if (v.length() > 10) { - int i = v.substring(10).indexOf("-"); - if (i == -1) - i = v.substring(10).indexOf("+"); - if (i == -1) - i = v.substring(10).indexOf("Z"); - v = i == -1 ? value : v.substring(0, 10+i); - } - if (v.length() > 10) - return new DateTimeType(value); + private List opAnd(List left, List right) { + if (left.isEmpty() && right.isEmpty()) + return new ArrayList(); + else if (isBoolean(left, false) || isBoolean(right, false)) + return makeBoolean(false); + else if (left.isEmpty() || right.isEmpty()) + return new ArrayList(); + else if (convertToBoolean(left) && convertToBoolean(right)) + return makeBoolean(true); else - return new DateType(value); - } - - - private Base resolveConstant(ExecutionContext context, String s) throws PathEngineException { - if (s.equals("%sct")) - return new StringType("http://snomed.info/sct"); - else if (s.equals("%loinc")) - return new StringType("http://loinc.org"); - else if (s.equals("%ucum")) - return new StringType("http://unitsofmeasure.org"); - else if (s.equals("%resource")) { - if (context.resource == null) - throw new PathEngineException("Cannot use %resource in this context"); - return context.resource; - } else if (s.equals("%context")) { - return context.context; - } else if (s.equals("%us-zip")) - return new StringType("[0-9]{5}(-[0-9]{4}){0,1}"); - else if (s.startsWith("%\"vs-")) - return new StringType("http://hl7.org/fhir/ValueSet/"+s.substring(5, s.length()-1)+""); - else if (s.startsWith("%\"cs-")) - return new StringType("http://hl7.org/fhir/"+s.substring(5, s.length()-1)+""); - else if (s.startsWith("%\"ext-")) - return new StringType("http://hl7.org/fhir/StructureDefinition/"+s.substring(6, s.length()-1)); - else if (hostServices == null) - throw new PathEngineException("Unknown fixed constant '"+s+"'"); - else - return hostServices.resolveConstant(context.appInfo, s.substring(1)); - } - - - private String processConstantString(String s) throws PathEngineException { - StringBuilder b = new StringBuilder(); - int i = 1; - while (i < s.length()-1) { - char ch = s.charAt(i); - if (ch == '\\') { - i++; - switch (s.charAt(i)) { - case 't': - b.append('\t'); - break; - case 'r': - b.append('\r'); - break; - case 'n': - b.append('\n'); - break; - case 'f': - b.append('\f'); - break; - case '\'': - b.append('\''); - break; - case '\\': - b.append('\\'); - break; - case '/': - b.append('/'); - break; - case 'u': - i++; - int uc = Integer.parseInt(s.substring(i, i+4), 16); - b.append((char) uc); - i = i + 3; - break; - default: - throw new PathEngineException("Unknown character escape \\"+s.charAt(i)); - } - i++; - } else { - b.append(ch); - i++; - } - } - return b.toString(); - } - - - private List operate(List left, Operation operation, List right) throws FHIRException { - switch (operation) { - case Equals: return opEquals(left, right); - case Equivalent: return opEquivalent(left, right); - case NotEquals: return opNotEquals(left, right); - case NotEquivalent: return opNotEquivalent(left, right); - case LessThen: return opLessThen(left, right); - case Greater: return opGreater(left, right); - case LessOrEqual: return opLessOrEqual(left, right); - case GreaterOrEqual: return opGreaterOrEqual(left, right); - case Union: return opUnion(left, right); - case In: return opIn(left, right); - case Contains: return opContains(left, right); - case Or: return opOr(left, right); - case And: return opAnd(left, right); - case Xor: return opXor(left, right); - case Implies: return opImplies(left, right); - case Plus: return opPlus(left, right); - case Times: return opTimes(left, right); - case Minus: return opMinus(left, right); - case Concatenate: return opConcatenate(left, right); - case DivideBy: return opDivideBy(left, right); - case Div: return opDiv(left, right); - case Mod: return opMod(left, right); - case Is: return opIs(left, right); - case As: return opAs(left, right); - default: - throw new Error("Not Done Yet: "+operation.toCode()); - } + return makeBoolean(false); } private List opAs(List left, List right) { @@ -1141,84 +1855,94 @@ public class FHIRPathEngine { return result; } - - private List opIs(List left, List right) { + private List opConcatenate(List left, List right) { List result = new ArrayList(); - if (left.size() != 1 || right.size() != 1) - result.add(new BooleanType(false)); - else { - String tn = convertToString(right); - result.add(new BooleanType(left.get(0).hasType(tn))); - } + result.add(new StringType(convertToString(left) + convertToString((right)))); return result; } - - private TypeDetails operateTypes(TypeDetails left, Operation operation, TypeDetails right) { - switch (operation) { - case Equals: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Equivalent: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case NotEquals: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case NotEquivalent: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case LessThen: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Greater: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case LessOrEqual: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case GreaterOrEqual: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Is: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case As: return new TypeDetails(CollectionStatus.SINGLETON, right.getTypes()); - case Union: return left.union(right); - case Or: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case And: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Xor: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Implies : return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Times: - TypeDetails result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType("integer"); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType("decimal"); - return result; - case DivideBy: - result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType("decimal"); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType("decimal"); - return result; - case Concatenate: - result = new TypeDetails(CollectionStatus.SINGLETON, ""); - return result; - case Plus: - result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType("integer"); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType("decimal"); - else if (left.hasType(worker, "string", "id", "code", "uri") && right.hasType(worker, "string", "id", "code", "uri")) - result.addType("string"); - return result; - case Minus: - result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType("integer"); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType("decimal"); - return result; - case Div: - case Mod: - result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType("integer"); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType("decimal"); - return result; - case In: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Contains: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - default: - return null; + private List opContains(List left, List right) { + boolean ans = true; + for (Base r : right) { + boolean f = false; + for (Base l : left) + if (doEquals(l, r)) { + f = true; + break; + } + if (!f) { + ans = false; + break; + } } + return makeBoolean(ans); } + private List opDiv(List left, List right) throws PathEngineException { + if (left.size() == 0) + throw new PathEngineException("Error performing div: left operand has no value"); + if (left.size() > 1) + throw new PathEngineException("Error performing div: left operand has more than one value"); + if (!left.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing div: left operand has the wrong type (%s)", left.get(0).fhirType())); + if (right.size() == 0) + throw new PathEngineException("Error performing div: right operand has no value"); + if (right.size() > 1) + throw new PathEngineException("Error performing div: right operand has more than one value"); + if (!right.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing div: right operand has the wrong type (%s)", right.get(0).fhirType())); + + List result = new ArrayList(); + Base l = left.get(0); + Base r = right.get(0); + + if (l.hasType("integer") && r.hasType("integer")) + result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) / Integer.parseInt(r.primitiveValue()))); + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { + Decimal d1; + try { + d1 = new Decimal(l.primitiveValue()); + Decimal d2 = new Decimal(r.primitiveValue()); + result.add(new IntegerType(d1.divInt(d2).asDecimal())); + } catch (UcumException e) { + throw new PathEngineException(e); + } + } else + throw new PathEngineException(String.format("Error performing div: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); + return result; + } + + private List opDivideBy(List left, List right) throws PathEngineException { + if (left.size() == 0) + throw new PathEngineException("Error performing /: left operand has no value"); + if (left.size() > 1) + throw new PathEngineException("Error performing /: left operand has more than one value"); + if (!left.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing -: left operand has the wrong type (%s)", left.get(0).fhirType())); + if (right.size() == 0) + throw new PathEngineException("Error performing /: right operand has no value"); + if (right.size() > 1) + throw new PathEngineException("Error performing /: right operand has more than one value"); + if (!right.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing /: right operand has the wrong type (%s)", right.get(0).fhirType())); + + List result = new ArrayList(); + Base l = left.get(0); + Base r = right.get(0); + + if (l.hasType("integer", "decimal", "unsignedInt", "positiveInt") && r.hasType("integer", "decimal", "unsignedInt", "positiveInt")) { + Decimal d1; + try { + d1 = new Decimal(l.primitiveValue()); + Decimal d2 = new Decimal(r.primitiveValue()); + result.add(new DecimalType(d1.divide(d2).asDecimal())); + } catch (UcumException e) { + throw new PathEngineException(e); + } + } else + throw new PathEngineException(String.format("Error performing /: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); + return result; + } private List opEquals(List left, List right) { if (left.size() != right.size()) @@ -1234,42 +1958,6 @@ public class FHIRPathEngine { return makeBoolean(res); } - private List opNotEquals(List left, List right) { - if (left.size() != right.size()) - return makeBoolean(true); - - boolean res = true; - for (int i = 0; i < left.size(); i++) { - if (!doEquals(left.get(i), right.get(i))) { - res = false; - break; - } - } - return makeBoolean(!res); - } - - private boolean doEquals(Base left, Base right) { - if (left.isPrimitive() && right.isPrimitive()) - return Base.equals(left.primitiveValue(), right.primitiveValue()); - else - return Base.compareDeep(left, right, false); - } - - private boolean doEquivalent(Base left, Base right) throws PathEngineException { - if (left.hasType("integer") && right.hasType("integer")) - return doEquals(left, right); - if (left.hasType("boolean") && right.hasType("boolean")) - return doEquals(left, right); - if (left.hasType("integer", "decimal", "unsignedInt", "positiveInt") && right.hasType("integer", "decimal", "unsignedInt", "positiveInt")) - return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); - if (left.hasType("date", "dateTime", "time", "instant") && right.hasType("date", "dateTime", "time", "instant")) - return compareDateTimeElements(left, right) == 0; - if (left.hasType("string", "id", "code", "uri") && right.hasType("string", "id", "code", "uri")) - return Utilities.equivalent(convertToString(left), convertToString(right)); - - throw new PathEngineException(String.format("Unable to determine equivalence between %s and %s", left.fhirType(), right.fhirType())); - } - private List opEquivalent(List left, List right) throws PathEngineException { if (left.size() != right.size()) return makeBoolean(false); @@ -1291,6 +1979,216 @@ public class FHIRPathEngine { return makeBoolean(res); } + private List opGreater(List left, List right) throws FHIRException { + if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { + Base l = left.get(0); + Base r = right.get(0); + if (l.hasType("string") && r.hasType("string")) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); + else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) + return makeBoolean(new Double(l.primitiveValue()) > new Double(r.primitiveValue())); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) > 0); + else if ((l.hasType("time")) && (r.hasType("time"))) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); + } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity")) { + List lUnit = left.get(0).listChildrenByName("unit"); + List rUnit = right.get(0).listChildrenByName("unit"); + if (Base.compareDeep(lUnit, rUnit, true)) { + return opGreater(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); + } else { + throw new InternalErrorException("Canonical Comparison isn't done yet"); + } + } + return new ArrayList(); + } + + private List opGreaterOrEqual(List left, List right) throws FHIRException { + if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { + Base l = left.get(0); + Base r = right.get(0); + if (l.hasType("string") && r.hasType("string")) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); + else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) + return makeBoolean(new Double(l.primitiveValue()) >= new Double(r.primitiveValue())); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) >= 0); + else if ((l.hasType("time")) && (r.hasType("time"))) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); + } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity")) { + List lUnit = left.get(0).listChildrenByName("unit"); + List rUnit = right.get(0).listChildrenByName("unit"); + if (Base.compareDeep(lUnit, rUnit, true)) { + return opGreaterOrEqual(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); + } else { + throw new InternalErrorException("Canonical Comparison isn't done yet"); + } + } + return new ArrayList(); + } + + private List opImplies(List left, List right) { + if (!convertToBoolean(left)) + return makeBoolean(true); + else if (right.size() == 0) + return new ArrayList(); + else + return makeBoolean(convertToBoolean(right)); + } + + private List opIn(List left, List right) { + boolean ans = true; + for (Base l : left) { + boolean f = false; + for (Base r : right) + if (doEquals(l, r)) { + f = true; + break; + } + if (!f) { + ans = false; + break; + } + } + return makeBoolean(ans); + } + + private List opIs(List left, List right) { + List result = new ArrayList(); + if (left.size() != 1 || right.size() != 1) + result.add(new BooleanType(false)); + else { + String tn = convertToString(right); + result.add(new BooleanType(left.get(0).hasType(tn))); + } + return result; + } + + private List opLessOrEqual(List left, List right) throws FHIRException { + if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { + Base l = left.get(0); + Base r = right.get(0); + if (l.hasType("string") && r.hasType("string")) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); + else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) + return makeBoolean(new Double(l.primitiveValue()) <= new Double(r.primitiveValue())); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) <= 0); + else if ((l.hasType("time")) && (r.hasType("time"))) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); + } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity")) { + List lUnits = left.get(0).listChildrenByName("unit"); + String lunit = lUnits.size() == 1 ? lUnits.get(0).primitiveValue() : null; + List rUnits = right.get(0).listChildrenByName("unit"); + String runit = rUnits.size() == 1 ? rUnits.get(0).primitiveValue() : null; + if ((lunit == null && runit == null) || lunit.equals(runit)) { + return opLessOrEqual(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); + } else { + throw new InternalErrorException("Canonical Comparison isn't done yet"); + } + } + return new ArrayList(); + } + + private List opLessThen(List left, List right) throws FHIRException { + if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { + Base l = left.get(0); + Base r = right.get(0); + if (l.hasType("string") && r.hasType("string")) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); + else if ((l.hasType("integer") || l.hasType("decimal")) && (r.hasType("integer") || r.hasType("decimal"))) + return makeBoolean(new Double(l.primitiveValue()) < new Double(r.primitiveValue())); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) < 0); + else if ((l.hasType("time")) && (r.hasType("time"))) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); + } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity")) { + List lUnit = left.get(0).listChildrenByName("unit"); + List rUnit = right.get(0).listChildrenByName("unit"); + if (Base.compareDeep(lUnit, rUnit, true)) { + return opLessThen(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); + } else { + throw new InternalErrorException("Canonical Comparison isn't done yet"); + } + } + return new ArrayList(); + } + + private List opMinus(List left, List right) throws PathEngineException { + if (left.size() == 0) + throw new PathEngineException("Error performing -: left operand has no value"); + if (left.size() > 1) + throw new PathEngineException("Error performing -: left operand has more than one value"); + if (!left.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing -: left operand has the wrong type (%s)", left.get(0).fhirType())); + if (right.size() == 0) + throw new PathEngineException("Error performing -: right operand has no value"); + if (right.size() > 1) + throw new PathEngineException("Error performing -: right operand has more than one value"); + if (!right.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing -: right operand has the wrong type (%s)", right.get(0).fhirType())); + + List result = new ArrayList(); + Base l = left.get(0); + Base r = right.get(0); + + if (l.hasType("integer") && r.hasType("integer")) + result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) - Integer.parseInt(r.primitiveValue()))); + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) + result.add(new DecimalType(new BigDecimal(l.primitiveValue()).subtract(new BigDecimal(r.primitiveValue())))); + else + throw new PathEngineException(String.format("Error performing -: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); + return result; + } + + private List opMod(List left, List right) throws PathEngineException { + if (left.size() == 0) + throw new PathEngineException("Error performing mod: left operand has no value"); + if (left.size() > 1) + throw new PathEngineException("Error performing mod: left operand has more than one value"); + if (!left.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing mod: left operand has the wrong type (%s)", left.get(0).fhirType())); + if (right.size() == 0) + throw new PathEngineException("Error performing mod: right operand has no value"); + if (right.size() > 1) + throw new PathEngineException("Error performing mod: right operand has more than one value"); + if (!right.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing mod: right operand has the wrong type (%s)", right.get(0).fhirType())); + + List result = new ArrayList(); + Base l = left.get(0); + Base r = right.get(0); + + if (l.hasType("integer") && r.hasType("integer")) + result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) % Integer.parseInt(r.primitiveValue()))); + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { + Decimal d1; + try { + d1 = new Decimal(l.primitiveValue()); + Decimal d2 = new Decimal(r.primitiveValue()); + result.add(new DecimalType(d1.modulo(d2).asDecimal())); + } catch (UcumException e) { + throw new PathEngineException(e); + } + } else + throw new PathEngineException(String.format("Error performing mod: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); + return result; + } + + private List opNotEquals(List left, List right) { + if (left.size() != right.size()) + return makeBoolean(true); + + boolean res = true; + for (int i = 0; i < left.size(); i++) { + if (!doEquals(left.get(i), right.get(i))) { + res = false; + break; + } + } + return makeBoolean(!res); + } + private List opNotEquivalent(List left, List right) throws PathEngineException { if (left.size() != right.size()) return makeBoolean(true); @@ -1312,152 +2210,15 @@ public class FHIRPathEngine { return makeBoolean(!res); } - private List opLessThen(List left, List right) throws FHIRException { - if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { - Base l = left.get(0); - Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); - else if ((l.hasType("integer") || l.hasType("decimal")) && (r.hasType("integer") || r.hasType("decimal"))) - return makeBoolean(new Double(l.primitiveValue()) < new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(compareDateTimeElements(l, r) < 0); - else if ((l.hasType("time")) && (r.hasType("time"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); - } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { - List lUnit = left.get(0).listChildrenByName("unit"); - List rUnit = right.get(0).listChildrenByName("unit"); - if (Base.compareDeep(lUnit, rUnit, true)) { - return opLessThen(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); - } else { - throw new InternalErrorException("Canonical Comparison isn't done yet"); - } - } - return new ArrayList(); - } - - private List opGreater(List left, List right) throws FHIRException { - if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { - Base l = left.get(0); - Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); - else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) - return makeBoolean(new Double(l.primitiveValue()) > new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(compareDateTimeElements(l, r) > 0); - else if ((l.hasType("time")) && (r.hasType("time"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); - } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { - List lUnit = left.get(0).listChildrenByName("unit"); - List rUnit = right.get(0).listChildrenByName("unit"); - if (Base.compareDeep(lUnit, rUnit, true)) { - return opGreater(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); - } else { - throw new InternalErrorException("Canonical Comparison isn't done yet"); - } - } - return new ArrayList(); - } - - private List opLessOrEqual(List left, List right) throws FHIRException { - if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { - Base l = left.get(0); - Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); - else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) - return makeBoolean(new Double(l.primitiveValue()) <= new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(compareDateTimeElements(l, r) <= 0); - else if ((l.hasType("time")) && (r.hasType("time"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); - } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { - List lUnits = left.get(0).listChildrenByName("unit"); - String lunit = lUnits.size() == 1 ? lUnits.get(0).primitiveValue() : null; - List rUnits = right.get(0).listChildrenByName("unit"); - String runit = rUnits.size() == 1 ? rUnits.get(0).primitiveValue() : null; - if ((lunit == null && runit == null) || lunit.equals(runit)) { - return opLessOrEqual(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); - } else { - throw new InternalErrorException("Canonical Comparison isn't done yet"); - } - } - return new ArrayList(); - } - - private List opGreaterOrEqual(List left, List right) throws FHIRException { - if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { - Base l = left.get(0); - Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); - else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) - return makeBoolean(new Double(l.primitiveValue()) >= new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(compareDateTimeElements(l, r) >= 0); - else if ((l.hasType("time")) && (r.hasType("time"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); - } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { - List lUnit = left.get(0).listChildrenByName("unit"); - List rUnit = right.get(0).listChildrenByName("unit"); - if (Base.compareDeep(lUnit, rUnit, true)) { - return opGreaterOrEqual(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); - } else { - throw new InternalErrorException("Canonical Comparison isn't done yet"); - } - } - return new ArrayList(); - } - - private int compareDateTimeElements(Base theL, Base theR) { - String dateLeftString = theL.primitiveValue(); - if (length(dateLeftString) > 10) { - DateTimeType dateLeft = new DateTimeType(dateLeftString); - dateLeft.setTimeZoneZulu(true); - dateLeftString = dateLeft.getValueAsString(); - } - String dateRightString = theR.primitiveValue(); - if (length(dateRightString) > 10) { - DateTimeType dateRight = new DateTimeType(dateRightString); - dateRight.setTimeZoneZulu(true); - dateRightString = dateRight.getValueAsString(); - } - return dateLeftString.compareTo(dateRightString); - } - - private List opIn(List left, List right) { - boolean ans = true; - for (Base l : left) { - boolean f = false; - for (Base r : right) - if (doEquals(l, r)) { - f = true; - break; - } - if (!f) { - ans = false; - break; - } - } - return makeBoolean(ans); - } - - private List opContains(List left, List right) { - boolean ans = true; - for (Base r : right) { - boolean f = false; - for (Base l : left) - if (doEquals(l, r)) { - f = true; - break; - } - if (!f) { - ans = false; - break; - } - } - return makeBoolean(ans); + private List opOr(List left, List right) { + if (left.isEmpty() && right.isEmpty()) + return new ArrayList(); + else if (convertToBoolean(left) || convertToBoolean(right)) + return makeBoolean(true); + else if (left.isEmpty() || right.isEmpty()) + return new ArrayList(); + else + return makeBoolean(false); } private List opPlus(List left, List right) throws PathEngineException { @@ -1515,12 +2276,6 @@ public class FHIRPathEngine { return result; } - private List opConcatenate(List left, List right) { - List result = new ArrayList(); - result.add(new StringType(convertToString(left) + convertToString((right)))); - return result; - } - private List opUnion(List left, List right) { List result = new ArrayList(); for (Base item : left) { @@ -1534,42 +2289,6 @@ public class FHIRPathEngine { return result; } - private boolean doContains(List list, Base item) { - for (Base test : list) - if (doEquals(test, item)) - return true; - return false; - } - - - private List opAnd(List left, List right) { - if (left.isEmpty() && right.isEmpty()) - return new ArrayList(); - else if (isBoolean(left, false) || isBoolean(right, false)) - return makeBoolean(false); - else if (left.isEmpty() || right.isEmpty()) - return new ArrayList(); - else if (convertToBoolean(left) && convertToBoolean(right)) - return makeBoolean(true); - else - return makeBoolean(false); - } - - private boolean isBoolean(List list, boolean b) { - return list.size() == 1 && list.get(0) instanceof BooleanType && ((BooleanType) list.get(0)).booleanValue() == b; - } - - private List opOr(List left, List right) { - if (left.isEmpty() && right.isEmpty()) - return new ArrayList(); - else if (convertToBoolean(left) || convertToBoolean(right)) - return makeBoolean(true); - else if (left.isEmpty() || right.isEmpty()) - return new ArrayList(); - else - return makeBoolean(false); - } - private List opXor(List left, List right) { if (left.isEmpty() || right.isEmpty()) return new ArrayList(); @@ -1577,147 +2296,383 @@ public class FHIRPathEngine { return makeBoolean(convertToBoolean(left) ^ convertToBoolean(right)); } - private List opImplies(List left, List right) { - if (!convertToBoolean(left)) - return makeBoolean(true); - else if (right.size() == 0) - return new ArrayList(); - else - return makeBoolean(convertToBoolean(right)); + private List operate(List left, Operation operation, List right) throws FHIRException { + switch (operation) { + case Equals: + return opEquals(left, right); + case Equivalent: + return opEquivalent(left, right); + case NotEquals: + return opNotEquals(left, right); + case NotEquivalent: + return opNotEquivalent(left, right); + case LessThen: + return opLessThen(left, right); + case Greater: + return opGreater(left, right); + case LessOrEqual: + return opLessOrEqual(left, right); + case GreaterOrEqual: + return opGreaterOrEqual(left, right); + case Union: + return opUnion(left, right); + case In: + return opIn(left, right); + case Contains: + return opContains(left, right); + case Or: + return opOr(left, right); + case And: + return opAnd(left, right); + case Xor: + return opXor(left, right); + case Implies: + return opImplies(left, right); + case Plus: + return opPlus(left, right); + case Times: + return opTimes(left, right); + case Minus: + return opMinus(left, right); + case Concatenate: + return opConcatenate(left, right); + case DivideBy: + return opDivideBy(left, right); + case Div: + return opDiv(left, right); + case Mod: + return opMod(left, right); + case Is: + return opIs(left, right); + case As: + return opAs(left, right); + default: + throw new Error("Not Done Yet: " + operation.toCode()); + } } + private TypeDetails operateTypes(TypeDetails left, Operation operation, TypeDetails right) { + switch (operation) { + case Equals: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Equivalent: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case NotEquals: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case NotEquivalent: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case LessThen: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Greater: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case LessOrEqual: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case GreaterOrEqual: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Is: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case As: + return new TypeDetails(CollectionStatus.SINGLETON, right.getTypes()); + case Union: + return left.union(right); + case Or: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case And: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Xor: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Implies: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Times: + TypeDetails result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType("integer"); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType("decimal"); + return result; + case DivideBy: + result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType("decimal"); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType("decimal"); + return result; + case Concatenate: + result = new TypeDetails(CollectionStatus.SINGLETON, ""); + return result; + case Plus: + result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType("integer"); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType("decimal"); + else if (left.hasType(worker, "string", "id", "code", "uri") && right.hasType(worker, "string", "id", "code", "uri")) + result.addType("string"); + return result; + case Minus: + result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType("integer"); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType("decimal"); + return result; + case Div: + case Mod: + result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType("integer"); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType("decimal"); + return result; + case In: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Contains: + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + default: + return null; + } + } - private List opMinus(List left, List right) throws PathEngineException { - if (left.size() == 0) - throw new PathEngineException("Error performing -: left operand has no value"); - if (left.size() > 1) - throw new PathEngineException("Error performing -: left operand has more than one value"); - if (!left.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing -: left operand has the wrong type (%s)", left.get(0).fhirType())); - if (right.size() == 0) - throw new PathEngineException("Error performing -: right operand has no value"); - if (right.size() > 1) - throw new PathEngineException("Error performing -: right operand has more than one value"); - if (!right.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing -: right operand has the wrong type (%s)", right.get(0).fhirType())); + private ExpressionNode organisePrecedence(FHIRLexer lexer, ExpressionNode node) { + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Times, Operation.DivideBy, Operation.Div, Operation.Mod)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Plus, Operation.Minus, Operation.Concatenate)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Union)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.LessThen, Operation.Greater, Operation.LessOrEqual, Operation.GreaterOrEqual)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Is)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Equals, Operation.Equivalent, Operation.NotEquals, Operation.NotEquivalent)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.And)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Xor, Operation.Or)); + // last: implies + return node; + } - List result = new ArrayList(); - Base l = left.get(0); - Base r = right.get(0); - - if (l.hasType("integer") && r.hasType("integer")) - result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) - Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) - result.add(new DecimalType(new BigDecimal(l.primitiveValue()).subtract(new BigDecimal(r.primitiveValue())))); - else - throw new PathEngineException(String.format("Error performing -: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); + /** + * Parse a path for later use using execute + * + * @param path + * @return + * @throws PathEngineException + * @throws Exception + */ + public ExpressionNode parse(String path) throws FHIRLexerException { + FHIRLexer lexer = new FHIRLexer(path); + if (lexer.done()) + throw lexer.error("Path cannot be empty"); + ExpressionNode result = parseExpression(lexer, true); + if (!lexer.done()) + throw lexer.error("Premature ExpressionNode termination at unexpected token \"" + lexer.getCurrent() + "\""); + result.check(); return result; } - private List opDivideBy(List left, List right) throws PathEngineException { - if (left.size() == 0) - throw new PathEngineException("Error performing /: left operand has no value"); - if (left.size() > 1) - throw new PathEngineException("Error performing /: left operand has more than one value"); - if (!left.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing -: left operand has the wrong type (%s)", left.get(0).fhirType())); - if (right.size() == 0) - throw new PathEngineException("Error performing /: right operand has no value"); - if (right.size() > 1) - throw new PathEngineException("Error performing /: right operand has more than one value"); - if (!right.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing /: right operand has the wrong type (%s)", right.get(0).fhirType())); + /** + * Parse a path that is part of some other syntax + * + * @param path + * @return + * @throws PathEngineException + * @throws Exception + */ + public ExpressionNode parse(FHIRLexer lexer) throws FHIRLexerException { + ExpressionNode result = parseExpression(lexer, true); + result.check(); + return result; + } - List result = new ArrayList(); - Base l = left.get(0); - Base r = right.get(0); + private ExpressionNode parseExpression(FHIRLexer lexer, boolean proximal) throws FHIRLexerException { + ExpressionNode result = new ExpressionNode(lexer.nextId()); + SourceLocation c = lexer.getCurrentStartLocation(); + result.setStart(lexer.getCurrentLocation()); + // special: + if (lexer.getCurrent().equals("-")) { + lexer.take(); + lexer.setCurrent("-" + lexer.getCurrent()); + } + if (lexer.getCurrent().equals("+")) { + lexer.take(); + lexer.setCurrent("+" + lexer.getCurrent()); + } + if (lexer.isConstant(false)) { + checkConstant(lexer.getCurrent(), lexer); + result.setConstant(lexer.take()); + result.setKind(Kind.Constant); + result.setEnd(lexer.getCurrentLocation()); + } else if ("(".equals(lexer.getCurrent())) { + lexer.next(); + result.setKind(Kind.Group); + result.setGroup(parseExpression(lexer, true)); + if (!")".equals(lexer.getCurrent())) + throw lexer.error("Found " + lexer.getCurrent() + " expecting a \")\""); + result.setEnd(lexer.getCurrentLocation()); + lexer.next(); + } else { + if (!lexer.isToken() && !lexer.getCurrent().startsWith("\"")) + throw lexer.error("Found " + lexer.getCurrent() + " expecting a token name"); + if (lexer.getCurrent().startsWith("\"")) + result.setName(lexer.readConstant("Path Name")); + else + result.setName(lexer.take()); + result.setEnd(lexer.getCurrentLocation()); + if (!result.checkName()) + throw lexer.error("Found " + result.getName() + " expecting a valid token name"); + if ("(".equals(lexer.getCurrent())) { + Function f = Function.fromCode(result.getName()); + FunctionDetails details = null; + if (f == null) { + if (hostServices != null) + details = hostServices.resolveFunction(result.getName()); + if (details == null) + throw lexer.error("The name " + result.getName() + " is not a valid function name"); + f = Function.Custom; + } + result.setKind(Kind.Function); + result.setFunction(f); + lexer.next(); + while (!")".equals(lexer.getCurrent())) { + result.getParameters().add(parseExpression(lexer, true)); + if (",".equals(lexer.getCurrent())) + lexer.next(); + else if (!")".equals(lexer.getCurrent())) + throw lexer.error("The token " + lexer.getCurrent() + " is not expected here - either a \",\" or a \")\" expected"); + } + result.setEnd(lexer.getCurrentLocation()); + lexer.next(); + checkParameters(lexer, c, result, details); + } else + result.setKind(Kind.Name); + } + ExpressionNode focus = result; + if ("[".equals(lexer.getCurrent())) { + lexer.next(); + ExpressionNode item = new ExpressionNode(lexer.nextId()); + item.setKind(Kind.Function); + item.setFunction(ExpressionNode.Function.Item); + item.getParameters().add(parseExpression(lexer, true)); + if (!lexer.getCurrent().equals("]")) + throw lexer.error("The token " + lexer.getCurrent() + " is not expected here - a \"]\" expected"); + lexer.next(); + result.setInner(item); + focus = item; + } + if (".".equals(lexer.getCurrent())) { + lexer.next(); + focus.setInner(parseExpression(lexer, false)); + } + result.setProximal(proximal); + if (proximal) { + while (lexer.isOp()) { + focus.setOperation(ExpressionNode.Operation.fromCode(lexer.getCurrent())); + focus.setOpStart(lexer.getCurrentStartLocation()); + focus.setOpEnd(lexer.getCurrentLocation()); + lexer.next(); + focus.setOpNext(parseExpression(lexer, false)); + focus = focus.getOpNext(); + } + result = organisePrecedence(lexer, result); + } + return result; + } - if (l.hasType("integer", "decimal", "unsignedInt", "positiveInt") && r.hasType("integer", "decimal", "unsignedInt", "positiveInt")) { - Decimal d1; - try { - d1 = new Decimal(l.primitiveValue()); - Decimal d2 = new Decimal(r.primitiveValue()); - result.add(new DecimalType(d1.divide(d2).asDecimal())); - } catch (UcumException e) { - throw new PathEngineException(e); + private List preOperate(List left, Operation operation) { + switch (operation) { + case And: + return isBoolean(left, false) ? makeBoolean(false) : null; + case Or: + return isBoolean(left, true) ? makeBoolean(true) : null; + case Implies: + return convertToBoolean(left) ? null : makeBoolean(true); + default: + return null; + } + } + + private Base processConstant(ExecutionContext context, String constant) throws PathEngineException { + if (constant.equals("true")) { + return new BooleanType(true); + } else if (constant.equals("false")) { + return new BooleanType(false); + } else if (constant.equals("{}")) { + return null; + } else if (Utilities.isInteger(constant)) { + return new IntegerType(constant); + } else if (Utilities.isDecimal(constant)) { + return new DecimalType(constant); + } else if (constant.startsWith("\'")) { + return new StringType(processConstantString(constant)); + } else if (constant.startsWith("%")) { + return resolveConstant(context, constant); + } else if (constant.startsWith("@")) { + return processDateConstant(context.appInfo, constant.substring(1)); + } else { + return new StringType(constant); + } + } + + private String processConstantString(String s) throws PathEngineException { + StringBuilder b = new StringBuilder(); + int i = 1; + while (i < s.length() - 1) { + char ch = s.charAt(i); + if (ch == '\\') { + i++; + switch (s.charAt(i)) { + case 't': + b.append('\t'); + break; + case 'r': + b.append('\r'); + break; + case 'n': + b.append('\n'); + break; + case 'f': + b.append('\f'); + break; + case '\'': + b.append('\''); + break; + case '\\': + b.append('\\'); + break; + case '/': + b.append('/'); + break; + case 'u': + i++; + int uc = Integer.parseInt(s.substring(i, i + 4), 16); + b.append((char) uc); + i = i + 3; + break; + default: + throw new PathEngineException("Unknown character escape \\" + s.charAt(i)); + } + i++; + } else { + b.append(ch); + i++; } } - else - throw new PathEngineException(String.format("Error performing /: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); - return result; + return b.toString(); } - private List opDiv(List left, List right) throws PathEngineException { - if (left.size() == 0) - throw new PathEngineException("Error performing div: left operand has no value"); - if (left.size() > 1) - throw new PathEngineException("Error performing div: left operand has more than one value"); - if (!left.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing div: left operand has the wrong type (%s)", left.get(0).fhirType())); - if (right.size() == 0) - throw new PathEngineException("Error performing div: right operand has no value"); - if (right.size() > 1) - throw new PathEngineException("Error performing div: right operand has more than one value"); - if (!right.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing div: right operand has the wrong type (%s)", right.get(0).fhirType())); - - List result = new ArrayList(); - Base l = left.get(0); - Base r = right.get(0); - - if (l.hasType("integer") && r.hasType("integer")) - result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) / Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { - Decimal d1; - try { - d1 = new Decimal(l.primitiveValue()); - Decimal d2 = new Decimal(r.primitiveValue()); - result.add(new IntegerType(d1.divInt(d2).asDecimal())); - } catch (UcumException e) { - throw new PathEngineException(e); - } + private Base processDateConstant(Object appInfo, String value) throws PathEngineException { + if (value.startsWith("T")) + return new TimeType(value.substring(1)); + String v = value; + if (v.length() > 10) { + int i = v.substring(10).indexOf("-"); + if (i == -1) + i = v.substring(10).indexOf("+"); + if (i == -1) + i = v.substring(10).indexOf("Z"); + v = i == -1 ? value : v.substring(0, 10 + i); } + if (v.length() > 10) + return new DateTimeType(value); else - throw new PathEngineException(String.format("Error performing div: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); - return result; + return new DateType(value); } - private List opMod(List left, List right) throws PathEngineException { - if (left.size() == 0) - throw new PathEngineException("Error performing mod: left operand has no value"); - if (left.size() > 1) - throw new PathEngineException("Error performing mod: left operand has more than one value"); - if (!left.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing mod: left operand has the wrong type (%s)", left.get(0).fhirType())); - if (right.size() == 0) - throw new PathEngineException("Error performing mod: right operand has no value"); - if (right.size() > 1) - throw new PathEngineException("Error performing mod: right operand has more than one value"); - if (!right.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing mod: right operand has the wrong type (%s)", right.get(0).fhirType())); - - List result = new ArrayList(); - Base l = left.get(0); - Base r = right.get(0); - - if (l.hasType("integer") && r.hasType("integer")) - result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) % Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { - Decimal d1; - try { - d1 = new Decimal(l.primitiveValue()); - Decimal d2 = new Decimal(r.primitiveValue()); - result.add(new DecimalType(d1.modulo(d2).asDecimal())); - } catch (UcumException e) { - throw new PathEngineException(e); - } - } - else - throw new PathEngineException(String.format("Error performing mod: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); - return result; - } - - private TypeDetails readConstantType(ExecutionTypeContext context, String constant) throws PathEngineException { if (constant.equals("true")) return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); @@ -1733,6 +2688,33 @@ public class FHIRPathEngine { return new TypeDetails(CollectionStatus.SINGLETON, "string"); } + private Base resolveConstant(ExecutionContext context, String s) throws PathEngineException { + if (s.equals("%sct")) + return new StringType("http://snomed.info/sct"); + else if (s.equals("%loinc")) + return new StringType("http://loinc.org"); + else if (s.equals("%ucum")) + return new StringType("http://unitsofmeasure.org"); + else if (s.equals("%resource")) { + if (context.resource == null) + throw new PathEngineException("Cannot use %resource in this context"); + return context.resource; + } else if (s.equals("%context")) { + return context.context; + } else if (s.equals("%us-zip")) + return new StringType("[0-9]{5}(-[0-9]{4}){0,1}"); + else if (s.startsWith("%\"vs-")) + return new StringType("http://hl7.org/fhir/ValueSet/" + s.substring(5, s.length() - 1) + ""); + else if (s.startsWith("%\"cs-")) + return new StringType("http://hl7.org/fhir/" + s.substring(5, s.length() - 1) + ""); + else if (s.startsWith("%\"ext-")) + return new StringType("http://hl7.org/fhir/StructureDefinition/" + s.substring(6, s.length() - 1)); + else if (hostServices == null) + throw new PathEngineException("Unknown fixed constant '" + s + "'"); + else + return hostServices.resolveConstant(context.appInfo, s.substring(1)); + } + private TypeDetails resolveConstantType(ExecutionTypeContext context, String s) throws PathEngineException { if (s.equals("%sct")) return new TypeDetails(CollectionStatus.SINGLETON, "string"); @@ -1757,1079 +2739,15 @@ public class FHIRPathEngine { else if (s.startsWith("%\"ext-")) return new TypeDetails(CollectionStatus.SINGLETON, "string"); else if (hostServices == null) - throw new PathEngineException("Unknown fixed constant type for '"+s+"'"); + throw new PathEngineException("Unknown fixed constant type for '" + s + "'"); else return hostServices.resolveConstantType(context.appInfo, s); } - private List execute(ExecutionContext context, Base item, ExpressionNode exp, boolean atEntry) throws FHIRException { - List result = new ArrayList(); - if (atEntry && Character.isUpperCase(exp.getName().charAt(0))) {// special case for start up - if (item.isResource() && item.fhirType().equals(exp.getName())) - result.add(item); - } else - getChildrenByName(item, exp.getName(), result); - if (result.size() == 0 && atEntry && context.appInfo != null) { - Base temp = hostServices.resolveConstant(context.appInfo, exp.getName()); - if (temp != null) { - result.add(temp); - } - } - return result; - } - - private TypeDetails executeContextType(ExecutionTypeContext context, String name) throws PathEngineException, DefinitionException { - if (hostServices == null) - throw new PathEngineException("Unable to resolve context reference since no host services are provided"); - return hostServices.resolveConstantType(context.appInfo, name); - } - - private TypeDetails executeType(String type, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { - if (atEntry && Character.isUpperCase(exp.getName().charAt(0)) && tail(type).equals(exp.getName())) // special case for start up - return new TypeDetails(CollectionStatus.SINGLETON, type); - TypeDetails result = new TypeDetails(null); - getChildTypesByName(type, exp.getName(), result); - return result; - } - - private String tail(String type) { - return type.contains("#") ? "" : type.substring(type.lastIndexOf("/")+1); + return type.contains("#") ? "" : type.substring(type.lastIndexOf("/") + 1); } - - @SuppressWarnings("unchecked") - private TypeDetails evaluateFunctionType(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp) throws PathEngineException, DefinitionException { - List paramTypes = new ArrayList(); - if (exp.getFunction() == Function.Is || exp.getFunction() == Function.As) - paramTypes.add(new TypeDetails(CollectionStatus.SINGLETON, "string")); - else - for (ExpressionNode expr : exp.getParameters()) { - if (exp.getFunction() == Function.Where || exp.getFunction() == Function.Select || exp.getFunction() == Function.Repeat) - paramTypes.add(executeType(changeThis(context, focus), focus, expr, true)); - else - paramTypes.add(executeType(context, focus, expr, true)); - } - switch (exp.getFunction()) { - case Empty : - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Not : - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Exists : - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case SubsetOf : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - } - case SupersetOf : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - } - case IsDistinct : - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Distinct : - return focus; - case Count : - return new TypeDetails(CollectionStatus.SINGLETON, "integer"); - case Where : - return focus; - case Select : - return anything(focus.getCollectionStatus()); - case All : - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Repeat : - return anything(focus.getCollectionStatus()); - case Item : { - checkOrdered(focus, "item"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); - return focus; - } - case As : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, exp.getParameters().get(0).getName()); - } - case Is : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - } - case Single : - return focus.toSingleton(); - case First : { - checkOrdered(focus, "first"); - return focus.toSingleton(); - } - case Last : { - checkOrdered(focus, "last"); - return focus.toSingleton(); - } - case Tail : { - checkOrdered(focus, "tail"); - return focus; - } - case Skip : { - checkOrdered(focus, "skip"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); - return focus; - } - case Take : { - checkOrdered(focus, "take"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); - return focus; - } - case Iif : { - TypeDetails types = new TypeDetails(null); - types.update(paramTypes.get(0)); - if (paramTypes.size() > 1) - types.update(paramTypes.get(1)); - return types; - } - case ToInteger : { - checkContextPrimitive(focus, "toInteger"); - return new TypeDetails(CollectionStatus.SINGLETON, "integer"); - } - case ToDecimal : { - checkContextPrimitive(focus, "toDecimal"); - return new TypeDetails(CollectionStatus.SINGLETON, "decimal"); - } - case ToString : { - checkContextPrimitive(focus, "toString"); - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - } - case Substring : { - checkContextString(focus, "subString"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer"), new TypeDetails(CollectionStatus.SINGLETON, "integer")); - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - } - case StartsWith : { - checkContextString(focus, "startsWith"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - } - case EndsWith : { - checkContextString(focus, "endsWith"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - } - case Matches : { - checkContextString(focus, "matches"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - } - case ReplaceMatches : { - checkContextString(focus, "replaceMatches"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - } - case Contains : { - checkContextString(focus, "contains"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - } - case Replace : { - checkContextString(focus, "replace"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - } - case Length : { - checkContextPrimitive(focus, "length"); - return new TypeDetails(CollectionStatus.SINGLETON, "integer"); - } - case Children : - return childTypes(focus, "*"); - case Descendants : - return childTypes(focus, "**"); - case MemberOf : { - checkContextCoded(focus, "memberOf"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - } - case Trace : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return focus; - } - case Today : - return new TypeDetails(CollectionStatus.SINGLETON, "date"); - case Now : - return new TypeDetails(CollectionStatus.SINGLETON, "dateTime"); - case Resolve : { - checkContextReference(focus, "resolve"); - return new TypeDetails(CollectionStatus.SINGLETON, "DomainResource"); - } - case Extension : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "Extension"); - } - case HasValue : - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Alias : - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return anything(CollectionStatus.SINGLETON); - case AliasAs : - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return focus; - case Custom : { - return hostServices.checkFunction(context.appInfo, exp.getName(), paramTypes); - } - default: - break; - } - throw new Error("not Implemented yet"); - } - - - private void checkParamTypes(String funcName, List paramTypes, TypeDetails... typeSet) throws PathEngineException { - int i = 0; - for (TypeDetails pt : typeSet) { - if (i == paramTypes.size()) - return; - TypeDetails actual = paramTypes.get(i); - i++; - for (String a : actual.getTypes()) { - if (!pt.hasType(worker, a)) - throw new PathEngineException("The parameter type '"+a+"' is not legal for "+funcName+" parameter "+Integer.toString(i)+". expecting "+pt.toString()); - } - } - } - - private void checkOrdered(TypeDetails focus, String name) throws PathEngineException { - if (focus.getCollectionStatus() == CollectionStatus.UNORDERED) - throw new PathEngineException("The function '"+name+"'() can only be used on ordered collections"); - } - - private void checkContextReference(TypeDetails focus, String name) throws PathEngineException { - if (!focus.hasType(worker, "string") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Reference")) - throw new PathEngineException("The function '"+name+"'() can only be used on string, uri, Reference"); - } - - - private void checkContextCoded(TypeDetails focus, String name) throws PathEngineException { - if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Coding") && !focus.hasType(worker, "CodeableConcept")) - throw new PathEngineException("The function '"+name+"'() can only be used on string, code, uri, Coding, CodeableConcept"); - } - - - private void checkContextString(TypeDetails focus, String name) throws PathEngineException { - if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "id")) - throw new PathEngineException("The function '"+name+"'() can only be used on string, uri, code, id, but found "+focus.describe()); - } - - - private void checkContextPrimitive(TypeDetails focus, String name) throws PathEngineException { - if (!focus.hasType(primitiveTypes)) - throw new PathEngineException("The function '"+name+"'() can only be used on "+primitiveTypes.toString()); - } - - - private TypeDetails childTypes(TypeDetails focus, String mask) throws PathEngineException, DefinitionException { - TypeDetails result = new TypeDetails(CollectionStatus.UNORDERED); - for (String f : focus.getTypes()) - getChildTypesByName(f, mask, result); - return result; - } - - private TypeDetails anything(CollectionStatus status) { - return new TypeDetails(status, allTypes.keySet()); - } - - // private boolean isPrimitiveType(String s) { - // return s.equals("boolean") || s.equals("integer") || s.equals("decimal") || s.equals("base64Binary") || s.equals("instant") || s.equals("string") || s.equals("uri") || s.equals("date") || s.equals("dateTime") || s.equals("time") || s.equals("code") || s.equals("oid") || s.equals("id") || s.equals("unsignedInt") || s.equals("positiveInt") || s.equals("markdown"); - // } - - private List evaluateFunction(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - switch (exp.getFunction()) { - case Empty : return funcEmpty(context, focus, exp); - case Not : return funcNot(context, focus, exp); - case Exists : return funcExists(context, focus, exp); - case SubsetOf : return funcSubsetOf(context, focus, exp); - case SupersetOf : return funcSupersetOf(context, focus, exp); - case IsDistinct : return funcIsDistinct(context, focus, exp); - case Distinct : return funcDistinct(context, focus, exp); - case Count : return funcCount(context, focus, exp); - case Where : return funcWhere(context, focus, exp); - case Select : return funcSelect(context, focus, exp); - case All : return funcAll(context, focus, exp); - case Repeat : return funcRepeat(context, focus, exp); - case Item : return funcItem(context, focus, exp); - case As : return funcAs(context, focus, exp); - case Is : return funcIs(context, focus, exp); - case Single : return funcSingle(context, focus, exp); - case First : return funcFirst(context, focus, exp); - case Last : return funcLast(context, focus, exp); - case Tail : return funcTail(context, focus, exp); - case Skip : return funcSkip(context, focus, exp); - case Take : return funcTake(context, focus, exp); - case Iif : return funcIif(context, focus, exp); - case ToInteger : return funcToInteger(context, focus, exp); - case ToDecimal : return funcToDecimal(context, focus, exp); - case ToString : return funcToString(context, focus, exp); - case Substring : return funcSubstring(context, focus, exp); - case StartsWith : return funcStartsWith(context, focus, exp); - case EndsWith : return funcEndsWith(context, focus, exp); - case Matches : return funcMatches(context, focus, exp); - case ReplaceMatches : return funcReplaceMatches(context, focus, exp); - case Contains : return funcContains(context, focus, exp); - case Replace : return funcReplace(context, focus, exp); - case Length : return funcLength(context, focus, exp); - case Children : return funcChildren(context, focus, exp); - case Descendants : return funcDescendants(context, focus, exp); - case MemberOf : return funcMemberOf(context, focus, exp); - case Trace : return funcTrace(context, focus, exp); - case Today : return funcToday(context, focus, exp); - case Now : return funcNow(context, focus, exp); - case Resolve : return funcResolve(context, focus, exp); - case Extension : return funcExtension(context, focus, exp); - case HasValue : return funcHasValue(context, focus, exp); - case AliasAs : return funcAliasAs(context, focus, exp); - case Alias : return funcAlias(context, focus, exp); - case Custom: { - List> params = new ArrayList>(); - for (ExpressionNode p : exp.getParameters()) - params.add(execute(context, focus, p, true)); - return hostServices.executeFunction(context.appInfo, exp.getName(), params); - } - default: - throw new Error("not Implemented yet"); - } - } - - private List funcAliasAs(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List nl = execute(context, focus, exp.getParameters().get(0), true); - String name = nl.get(0).primitiveValue(); - context.addAlias(name, focus); - return focus; - } - - private List funcAlias(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List nl = execute(context, focus, exp.getParameters().get(0), true); - String name = nl.get(0).primitiveValue(); - List res = new ArrayList(); - Base b = context.getAlias(name); - if (b != null) - res.add(b); - return res; - - } - - private List funcAll(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - if (exp.getParameters().size() == 1) { - List result = new ArrayList(); - List pc = new ArrayList(); - boolean all = true; - for (Base item : focus) { - pc.clear(); - pc.add(item); - if (!convertToBoolean(execute(changeThis(context, item), pc, exp.getParameters().get(0), true))) { - all = false; - break; - } - } - result.add(new BooleanType(all)); - return result; - } else {// (exp.getParameters().size() == 0) { - List result = new ArrayList(); - boolean all = true; - for (Base item : focus) { - boolean v = false; - if (item instanceof BooleanType) { - v = ((BooleanType) item).booleanValue(); - } else - v = item != null; - if (!v) { - all = false; - break; - } - } - result.add(new BooleanType(all)); - return result; - } - } - - - private ExecutionContext changeThis(ExecutionContext context, Base newThis) { - return new ExecutionContext(context.appInfo, context.resource, context.context, context.aliases, newThis); - } - - private ExecutionTypeContext changeThis(ExecutionTypeContext context, TypeDetails newThis) { - return new ExecutionTypeContext(context.appInfo, context.resource, context.context, newThis); - } - - - private List funcNow(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(DateTimeType.now()); - return result; - } - - - private List funcToday(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new DateType(new Date(), TemporalPrecisionEnum.DAY)); - return result; - } - - - private List funcMemberOf(ExecutionContext context, List focus, ExpressionNode exp) { - throw new Error("not Implemented yet"); - } - - - private List funcDescendants(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List current = new ArrayList(); - current.addAll(focus); - List added = new ArrayList(); - boolean more = true; - while (more) { - added.clear(); - for (Base item : current) { - getChildrenByName(item, "*", added); - } - more = !added.isEmpty(); - result.addAll(added); - current.clear(); - current.addAll(added); - } - return result; - } - - - private List funcChildren(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - for (Base b : focus) - getChildrenByName(b, "*", result); - return result; - } - - - private List funcReplace(ExecutionContext context, List focus, ExpressionNode exp) { - throw new Error("not Implemented yet"); - } - - - private List funcReplaceMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) - result.add(new BooleanType(convertToString(focus.get(0)).contains(sw))); - else - result.add(new BooleanType(false)); - return result; - } - - - private List funcEndsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) - result.add(new BooleanType(convertToString(focus.get(0)).endsWith(sw))); - else - result.add(new BooleanType(false)); - return result; - } - - - private List funcToString(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new StringType(convertToString(focus))); - return result; - } - - - private List funcToDecimal(ExecutionContext context, List focus, ExpressionNode exp) { - String s = convertToString(focus); - List result = new ArrayList(); - if (Utilities.isDecimal(s)) - result.add(new DecimalType(s)); - return result; - } - - - private List funcIif(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List n1 = execute(context, focus, exp.getParameters().get(0), true); - Boolean v = convertToBoolean(n1); - - if (v) - return execute(context, focus, exp.getParameters().get(1), true); - else if (exp.getParameters().size() < 3) - return new ArrayList(); - else - return execute(context, focus, exp.getParameters().get(2), true); - } - - - private List funcTake(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List n1 = execute(context, focus, exp.getParameters().get(0), true); - int i1 = Integer.parseInt(n1.get(0).primitiveValue()); - - List result = new ArrayList(); - for (int i = 0; i < Math.min(focus.size(), i1); i++) - result.add(focus.get(i)); - return result; - } - - - private List funcSingle(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { - if (focus.size() == 1) - return focus; - throw new PathEngineException(String.format("Single() : checking for 1 item but found %d items", focus.size())); - } - - - private List funcIs(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { - List result = new ArrayList(); - if (focus.size() == 0 || focus.size() > 1) - result.add(new BooleanType(false)); - else { - String tn = exp.getParameters().get(0).getName(); - result.add(new BooleanType(focus.get(0).hasType(tn))); - } - return result; - } - - - private List funcAs(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - String tn = exp.getParameters().get(0).getName(); - for (Base b : focus) - if (b.hasType(tn)) - result.add(b); - return result; - } - - - private List funcRepeat(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List current = new ArrayList(); - current.addAll(focus); - List added = new ArrayList(); - boolean more = true; - while (more) { - added.clear(); - List pc = new ArrayList(); - for (Base item : current) { - pc.clear(); - pc.add(item); - added.addAll(execute(changeThis(context, item), pc, exp.getParameters().get(0), false)); - } - more = !added.isEmpty(); - result.addAll(added); - current.clear(); - current.addAll(added); - } - return result; - } - - - - private List funcIsDistinct(ExecutionContext context, List focus, ExpressionNode exp) { - if (focus.size() <= 1) - return makeBoolean(true); - - boolean distinct = true; - for (int i = 0; i < focus.size(); i++) { - for (int j = i+1; j < focus.size(); j++) { - if (doEquals(focus.get(j), focus.get(i))) { - distinct = false; - break; - } - } - } - return makeBoolean(distinct); - } - - - private List funcSupersetOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List target = execute(context, focus, exp.getParameters().get(0), true); - - boolean valid = true; - for (Base item : target) { - boolean found = false; - for (Base t : focus) { - if (Base.compareDeep(item, t, false)) { - found = true; - break; - } - } - if (!found) { - valid = false; - break; - } - } - List result = new ArrayList(); - result.add(new BooleanType(valid)); - return result; - } - - - private List funcSubsetOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List target = execute(context, focus, exp.getParameters().get(0), true); - - boolean valid = true; - for (Base item : focus) { - boolean found = false; - for (Base t : target) { - if (Base.compareDeep(item, t, false)) { - found = true; - break; - } - } - if (!found) { - valid = false; - break; - } - } - List result = new ArrayList(); - result.add(new BooleanType(valid)); - return result; - } - - - private List funcExists(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new BooleanType(!ElementUtil.isEmpty(focus))); - return result; - } - - - private List funcResolve(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - for (Base item : focus) { - if (hostServices != null) { - String s = convertToString(item); - if (item.fhirType().equals("Reference")) { - Property p = item.getChildByName("reference"); - if (p.hasValues()) - s = convertToString(p.getValues().get(0)); - } - Base res = null; - if (s.startsWith("#")) { - String id = s.substring(1); - Property p = context.resource.getChildByName("contained"); - for (Base c : p.getValues()) { - if (id.equals(c.getIdBase())) - res = c; - } - } else - res = hostServices.resolveReference(context.appInfo, s); - if (res != null) - result.add(res); - } - } - return result; - } - - private List funcExtension(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List nl = execute(context, focus, exp.getParameters().get(0), true); - String url = nl.get(0).primitiveValue(); - - for (Base item : focus) { - List ext = new ArrayList(); - getChildrenByName(item, "extension", ext); - getChildrenByName(item, "modifierExtension", ext); - for (Base ex : ext) { - List vl = new ArrayList(); - getChildrenByName(ex, "url", vl); - if (convertToString(vl).equals(url)) - result.add(ex); - } - } - return result; - } - - private List funcTrace(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List nl = execute(context, focus, exp.getParameters().get(0), true); - String name = nl.get(0).primitiveValue(); - - log(name, focus); - return focus; - } - - private List funcDistinct(ExecutionContext context, List focus, ExpressionNode exp) { - if (focus.size() <= 1) - return focus; - - List result = new ArrayList(); - for (int i = 0; i < focus.size(); i++) { - boolean found = false; - for (int j = i+1; j < focus.size(); j++) { - if (doEquals(focus.get(j), focus.get(i))) { - found = true; - break; - } - } - if (!found) - result.add(focus.get(i)); - } - return result; - } - - private List funcMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) { - String st = convertToString(focus.get(0)); - if (Utilities.noString(st)) - result.add(new BooleanType(false)); - else - result.add(new BooleanType(st.matches(sw))); - } else - result.add(new BooleanType(false)); - return result; - } - - private List funcContains(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) { - String st = convertToString(focus.get(0)); - if (Utilities.noString(st)) - result.add(new BooleanType(false)); - else - result.add(new BooleanType(st.contains(sw))); - } else - result.add(new BooleanType(false)); - return result; - } - - private List funcLength(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() == 1) { - String s = convertToString(focus.get(0)); - result.add(new IntegerType(s.length())); - } - return result; - } - - private List funcHasValue(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() == 1) { - String s = convertToString(focus.get(0)); - result.add(new BooleanType(!Utilities.noString(s))); - } - return result; - } - - private List funcStartsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) - result.add(new BooleanType(convertToString(focus.get(0)).startsWith(sw))); - else - result.add(new BooleanType(false)); - return result; - } - - private List funcSubstring(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List n1 = execute(context, focus, exp.getParameters().get(0), true); - int i1 = Integer.parseInt(n1.get(0).primitiveValue()); - int i2 = -1; - if (exp.parameterCount() == 2) { - List n2 = execute(context, focus, exp.getParameters().get(1), true); - i2 = Integer.parseInt(n2.get(0).primitiveValue()); - } - - if (focus.size() == 1) { - String sw = convertToString(focus.get(0)); - String s; - if (i1 < 0 || i1 >= sw.length()) - return new ArrayList(); - if (exp.parameterCount() == 2) - s = sw.substring(i1, Math.min(sw.length(), i1+i2)); - else - s = sw.substring(i1); - if (!Utilities.noString(s)) - result.add(new StringType(s)); - } - return result; - } - - private List funcToInteger(ExecutionContext context, List focus, ExpressionNode exp) { - String s = convertToString(focus); - List result = new ArrayList(); - if (Utilities.isInteger(s)) - result.add(new IntegerType(s)); - return result; - } - - private List funcCount(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new IntegerType(focus.size())); - return result; - } - - private List funcSkip(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List n1 = execute(context, focus, exp.getParameters().get(0), true); - int i1 = Integer.parseInt(n1.get(0).primitiveValue()); - - List result = new ArrayList(); - for (int i = i1; i < focus.size(); i++) - result.add(focus.get(i)); - return result; - } - - private List funcTail(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - for (int i = 1; i < focus.size(); i++) - result.add(focus.get(i)); - return result; - } - - private List funcLast(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() > 0) - result.add(focus.get(focus.size()-1)); - return result; - } - - private List funcFirst(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() > 0) - result.add(focus.get(0)); - return result; - } - - - private List funcWhere(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List pc = new ArrayList(); - for (Base item : focus) { - pc.clear(); - pc.add(item); - if (convertToBoolean(execute(changeThis(context, item), pc, exp.getParameters().get(0), true))) - result.add(item); - } - return result; - } - - private List funcSelect(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List pc = new ArrayList(); - for (Base item : focus) { - pc.clear(); - pc.add(item); - result.addAll(execute(changeThis(context, item), pc, exp.getParameters().get(0), true)); - } - return result; - } - - - private List funcItem(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String s = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - if (Utilities.isInteger(s) && Integer.parseInt(s) < focus.size()) - result.add(focus.get(Integer.parseInt(s))); - return result; - } - - private List funcEmpty(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new BooleanType(ElementUtil.isEmpty(focus))); - return result; - } - - private List funcNot(ExecutionContext context, List focus, ExpressionNode exp) { - return makeBoolean(!convertToBoolean(focus)); - } - - public class ElementDefinitionMatch { - private ElementDefinition definition; - private String fixedType; - public ElementDefinitionMatch(ElementDefinition definition, String fixedType) { - super(); - this.definition = definition; - this.fixedType = fixedType; - } - public ElementDefinition getDefinition() { - return definition; - } - public String getFixedType() { - return fixedType; - } - - } - - private void getChildTypesByName(String type, String name, TypeDetails result) throws PathEngineException, DefinitionException { - if (Utilities.noString(type)) - throw new PathEngineException("No type provided in BuildToolPathEvaluator.getChildTypesByName"); - if (type.equals("http://hl7.org/fhir/StructureDefinition/xhtml")) - return; - String url = null; - if (type.contains("#")) { - url = type.substring(0, type.indexOf("#")); - } else { - url = type; - } - String tail = ""; - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, url); - if (sd == null) - throw new DefinitionException("Unknown type "+type); // this really is an error, because we can only get to here if the internal infrastrucgture is wrong - List sdl = new ArrayList(); - ElementDefinitionMatch m = null; - if (type.contains("#")) - m = getElementDefinition(sd, type.substring(type.indexOf("#")+1), false); - if (m != null && hasDataType(m.definition)) { - if (m.fixedType != null) - { - StructureDefinition dt = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+m.fixedType); - if (dt == null) - throw new DefinitionException("unknown data type "+m.fixedType); - sdl.add(dt); - } else - for (TypeRefComponent t : m.definition.getType()) { - StructureDefinition dt = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+t.getCode()); - if (dt == null) - throw new DefinitionException("unknown data type "+t.getCode()); - sdl.add(dt); - } - } else { - sdl.add(sd); - if (type.contains("#")) { - tail = type.substring(type.indexOf("#")+1); - tail = tail.substring(tail.indexOf(".")); - } - } - - for (StructureDefinition sdi : sdl) { - String path = sdi.getSnapshot().getElement().get(0).getPath()+tail+"."; - if (name.equals("**")) { - assert(result.getCollectionStatus() == CollectionStatus.UNORDERED); - for (ElementDefinition ed : sdi.getSnapshot().getElement()) { - if (ed.getPath().startsWith(path)) - for (TypeRefComponent t : ed.getType()) { - if (t.hasCode() && t.getCodeElement().hasValue()) { - String tn = null; - if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) - tn = sdi.getType()+"#"+ed.getPath(); - else - tn = t.getCode(); - if (t.getCode().equals("Resource")) { - for (String rn : worker.getResourceNames()) { - if (!result.hasType(worker, rn)) { - getChildTypesByName(result.addType(rn), "**", result); - } - } - } else if (!result.hasType(worker, tn)) { - getChildTypesByName(result.addType(tn), "**", result); - } - } - } - } - } else if (name.equals("*")) { - assert(result.getCollectionStatus() == CollectionStatus.UNORDERED); - for (ElementDefinition ed : sdi.getSnapshot().getElement()) { - if (ed.getPath().startsWith(path) && !ed.getPath().substring(path.length()).contains(".")) - for (TypeRefComponent t : ed.getType()) { - if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) - result.addType(sdi.getType()+"#"+ed.getPath()); - else if (t.getCode().equals("Resource")) - result.addTypes(worker.getResourceNames()); - else - result.addType(t.getCode()); - } - } - } else { - path = sdi.getSnapshot().getElement().get(0).getPath()+tail+"."+name; - - ElementDefinitionMatch ed = getElementDefinition(sdi, path, false); - if (ed != null) { - if (!Utilities.noString(ed.getFixedType())) - result.addType(ed.getFixedType()); - else - for (TypeRefComponent t : ed.getDefinition().getType()) { - if (Utilities.noString(t.getCode())) - break; // throw new PathEngineException("Illegal reference to primitive value attribute @ "+path); - - ProfiledType pt = null; - if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) - pt = new ProfiledType(sdi.getUrl()+"#"+path); - else if (t.getCode().equals("Resource")) - result.addTypes(worker.getResourceNames()); - else - pt = new ProfiledType(t.getCode()); - if (pt != null) { - if (t.hasProfile()) - pt.addProfile(t.getProfile()); - if (ed.getDefinition().hasBinding()) - pt.addBinding(ed.getDefinition().getBinding()); - result.addType(pt); - } - } - } - } - } - } - - private ElementDefinitionMatch getElementDefinition(StructureDefinition sd, String path, boolean allowTypedName) throws PathEngineException { - for (ElementDefinition ed : sd.getSnapshot().getElement()) { - if (ed.getPath().equals(path)) { - if (ed.hasContentReference()) { - return getElementDefinitionById(sd, ed.getContentReference()); - } else - return new ElementDefinitionMatch(ed, null); - } - if (ed.getPath().endsWith("[x]") && path.startsWith(ed.getPath().substring(0, ed.getPath().length()-3)) && path.length() == ed.getPath().length()-3) - return new ElementDefinitionMatch(ed, null); - if (allowTypedName && ed.getPath().endsWith("[x]") && path.startsWith(ed.getPath().substring(0, ed.getPath().length()-3)) && path.length() > ed.getPath().length()-3) { - String s = Utilities.uncapitalize(path.substring(ed.getPath().length()-3)); - if (primitiveTypes.contains(s)) - return new ElementDefinitionMatch(ed, s); - else - return new ElementDefinitionMatch(ed, path.substring(ed.getPath().length()-3)); - } - if (ed.getPath().contains(".") && path.startsWith(ed.getPath()+".") && (ed.getType().size() > 0) && !isAbstractType(ed.getType())) { - // now we walk into the type. - if (ed.getType().size() > 1) // if there's more than one type, the test above would fail this - throw new PathEngineException("Internal typing issue...."); - StructureDefinition nsd = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+ed.getType().get(0).getCode()); - if (nsd == null) - throw new PathEngineException("Unknown type "+ed.getType().get(0).getCode()); - return getElementDefinition(nsd, nsd.getId()+path.substring(ed.getPath().length()), allowTypedName); - } - if (ed.hasContentReference() && path.startsWith(ed.getPath()+".")) { - ElementDefinitionMatch m = getElementDefinitionById(sd, ed.getContentReference()); - return getElementDefinition(sd, m.definition.getPath()+path.substring(ed.getPath().length()), allowTypedName); - } - } - return null; - } - - private boolean isAbstractType(List list) { - return list.size() != 1 ? true : Utilities.existsInList(list.get(0).getCode(), "Element", "BackboneElement", "Resource", "DomainResource"); -} - - - private boolean hasType(ElementDefinition ed, String s) { - for (TypeRefComponent t : ed.getType()) - if (s.equalsIgnoreCase(t.getCode())) - return true; - return false; - } - - private boolean hasDataType(ElementDefinition ed) { - return ed.hasType() && !(ed.getType().get(0).getCode().equals("Element") || ed.getType().get(0).getCode().equals("BackboneElement")); - } - - private ElementDefinitionMatch getElementDefinitionById(StructureDefinition sd, String ref) { - for (ElementDefinition ed : sd.getSnapshot().getElement()) { - if (ref.equals("#"+ed.getId())) - return new ElementDefinitionMatch(ed, null); - } - return null; - } - - - public boolean hasLog() { - return log != null && log.length() > 0; - } - - public String takeLog() { if (!hasLog()) return ""; @@ -2838,4 +2756,174 @@ public class FHIRPathEngine { return s; } + // if the fhir path expressions are allowed to use constants beyond those defined in the specification + // the application can implement them by providing a constant resolver + public interface IEvaluationContext { + /** + * Check the function parameters, and throw an error if they are incorrect, or return the type for the function + * + * @param functionName + * @param parameters + * @return + */ + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException; + + /** + * @param appContext + * @param functionName + * @param parameters + * @return + */ + public List executeFunction(Object appContext, String functionName, List> parameters); + + /** + * when the .log() function is called + * + * @param argument + * @param focus + * @return + */ + public boolean log(String argument, List focus); + + /** + * A constant reference - e.g. a reference to a name that must be resolved in context. + * The % will be removed from the constant name before this is invoked. + *

    + * This will also be called if the host invokes the FluentPath engine with a context of null + * + * @param appContext - content passed into the fluent path engine + * @param name - name reference to resolve + * @return the value of the reference (or null, if it's not valid, though can throw an exception if desired) + */ + public Base resolveConstant(Object appContext, String name) throws PathEngineException; + + // extensibility for functions + + public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException; + + /** + * @param functionName + * @return null if the function is not known + */ + public FunctionDetails resolveFunction(String functionName); + + /** + * Implementation of resolve() function. Passed a string, return matching resource, if one is known - else null + * + * @param appInfo + * @param url + * @return + */ + public Base resolveReference(Object appContext, String url); + + public class FunctionDetails { + private String description; + private int minParameters; + private int maxParameters; + + public FunctionDetails(String description, int minParameters, int maxParameters) { + super(); + this.description = description; + this.minParameters = minParameters; + this.maxParameters = maxParameters; + } + + public String getDescription() { + return description; + } + + public int getMaxParameters() { + return maxParameters; + } + + public int getMinParameters() { + return minParameters; + } + + } + } + + private class ExecutionContext { + private Object appInfo; + private Base resource; + private Base context; + private Base thisItem; + private Map aliases; + + public ExecutionContext(Object appInfo, Base resource, Base context, Map aliases, Base thisItem) { + this.appInfo = appInfo; + this.context = context; + this.resource = resource; + this.aliases = aliases; + this.thisItem = thisItem; + } + + public void addAlias(String name, List focus) throws FHIRException { + if (aliases == null) + aliases = new HashMap(); + else + aliases = new HashMap(aliases); // clone it, since it's going to change + if (focus.size() > 1) + throw new FHIRException("Attempt to alias a collection, not a singleton"); + aliases.put(name, focus.size() == 0 ? null : focus.get(0)); + } + + public Base getAlias(String name) { + return aliases == null ? null : aliases.get(name); + } + + public Base getResource() { + return resource; + } + + public Base getThisItem() { + return thisItem; + } + } + + private class ExecutionTypeContext { + private Object appInfo; + private String resource; + private String context; + private TypeDetails thisItem; + + + public ExecutionTypeContext(Object appInfo, String resource, String context, TypeDetails thisItem) { + super(); + this.appInfo = appInfo; + this.resource = resource; + this.context = context; + this.thisItem = thisItem; + + } + + public String getResource() { + return resource; + } + + public TypeDetails getThisItem() { + return thisItem; + } + } + + public class ElementDefinitionMatch { + private ElementDefinition definition; + private String fixedType; + + public ElementDefinitionMatch(ElementDefinition definition, String fixedType) { + super(); + this.definition = definition; + this.fixedType = fixedType; + } + + public ElementDefinition getDefinition() { + return definition; + } + + public String getFixedType() { + return fixedType; + } + + } + } 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 abf39e1c0df..80a04f1f097 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 @@ -32,6 +32,12 @@ public class FhirContextDstu3Test { assertEquals(FhirVersionEnum.DSTU3, ctx.getVersion().getVersion()); } + @Test + public void testRuntimeSearchParamToString() { + String val = ourCtx.getResourceDefinition("Patient").getSearchParam("gender").toString(); + assertEquals("RuntimeSearchParam[base=[Patient],name=gender,path=Patient.gender,id=,uri=]", val); + } + @Test public void testCustomTypeDoesntBecomeDefault() { FhirContext ctx = FhirContext.forDstu3(); @@ -69,7 +75,7 @@ public class FhirContextDstu3Test { final FhirContext ctx = FhirContext.forDstu3(); final int numThreads = 40; - final List exceptions = Collections.synchronizedList(new ArrayList()); + final List exceptions = Collections.synchronizedList(new ArrayList<>()); final ExecutorService threadPool = Executors.newFixedThreadPool(numThreads); try { final CountDownLatch threadsReady = new CountDownLatch(numThreads); @@ -77,19 +83,17 @@ public class FhirContextDstu3Test { for (int i = 0; i < numThreads; i++) { threadPool.submit( - new Runnable() { - public void run() { - threadsReady.countDown(); - try { - threadsReady.await(); - RuntimeResourceDefinition def = ctx.getResourceDefinition("patient"); - ourLog.info(def.toString()); - assertNotNull(def); - } catch (final Exception e) { - exceptions.add(e); - } - threadsFinished.countDown(); + () -> { + threadsReady.countDown(); + try { + threadsReady.await(); + RuntimeResourceDefinition def = ctx.getResourceDefinition("patient"); + ourLog.info(def.toString()); + assertNotNull(def); + } catch (final Exception e) { + exceptions.add(e); } + threadsFinished.countDown(); } ); } @@ -108,18 +112,15 @@ public class FhirContextDstu3Test { * See #794 */ @Test - public void testInitializeThreadSafety2() throws InterruptedException { + public void testInitializeThreadSafety2() { final FhirContext dstu3FhirContext = FhirContext.forDstu3(); final AtomicInteger count = new AtomicInteger(); for (int i = 0; i < 10; i++) { - new Thread(new Runnable() { - @Override - public void run() { - OperationOutcomeUtil.newInstance(dstu3FhirContext); - ourLog.info("Have finished {}", count.incrementAndGet()); - } + new Thread(() -> { + OperationOutcomeUtil.newInstance(dstu3FhirContext); + ourLog.info("Have finished {}", count.incrementAndGet()); }).start(); } diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java index 80e2f7400bd..4601d42ed5e 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java @@ -1,11 +1,10 @@ package org.hl7.fhir.r4.utils; //import ca.uhn.fhir.model.api.TemporalPrecisionEnum; + import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.util.ElementUtil; - import org.apache.commons.lang3.NotImplementedException; -import org.apache.http.protocol.ExecutionContext; import org.fhir.ucum.Decimal; import org.fhir.ucum.Pair; import org.fhir.ucum.UcumException; @@ -25,193 +24,17 @@ import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext.FunctionDetails; import org.hl7.fhir.utilities.Utilities; import java.math.BigDecimal; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.*; -import static org.apache.commons.lang3.StringUtils.length; - /** - * * @author Grahame Grieve - * */ public class FHIRPathEngine { - private class FHIRConstant extends Base { - - private static final long serialVersionUID = -8933773658248269439L; - private String value; - - public FHIRConstant(String value) { - this.value = value; - } - - @Override - public String fhirType() { - return "%constant"; - } - - @Override - protected void listChildren(List result) { - } - - @Override - public String getIdBase() { - return null; - } - - @Override - public void setIdBase(String value) { - } - - public String getValue() { - return value; - } - } - - private class ClassTypeInfo extends Base { - private static final long serialVersionUID = 4909223114071029317L; - private Base instance; - - public ClassTypeInfo(Base instance) { - super(); - this.instance = instance; - } - - @Override - public String fhirType() { - return "ClassInfo"; - } - - @Override - protected void listChildren(List result) { - } - - @Override - public String getIdBase() { - return null; - } - - @Override - public void setIdBase(String value) { - } - public Base[] getProperty(int hash, String name, boolean checkValid) throws FHIRException { - if (name.equals("name")) - return new Base[]{new StringType(getName())}; - else if (name.equals("namespace")) - return new Base[]{new StringType(getNamespace())}; - else - return super.getProperty(hash, name, checkValid); - } - - private String getNamespace() { - if ((instance instanceof Resource)) - return "FHIR"; - else if (!(instance instanceof Element) || ((Element)instance).isDisallowExtensions()) - return "System"; - else - return "FHIR"; - } - - private String getName() { - if ((instance instanceof Resource)) - return instance.fhirType(); - else if (!(instance instanceof Element) || ((Element)instance).isDisallowExtensions()) - return Utilities.capitalize(instance.fhirType()); - else - return instance.fhirType(); - } - } - private IWorkerContext worker; private IEvaluationContext hostServices; private StringBuilder log = new StringBuilder(); private Set primitiveTypes = new HashSet(); private Map allTypes = new HashMap(); - - // if the fhir path expressions are allowed to use constants beyond those defined in the specification - // the application can implement them by providing a constant resolver - public interface IEvaluationContext { - public class FunctionDetails { - private String description; - private int minParameters; - private int maxParameters; - public FunctionDetails(String description, int minParameters, int maxParameters) { - super(); - this.description = description; - this.minParameters = minParameters; - this.maxParameters = maxParameters; - } - public String getDescription() { - return description; - } - public int getMinParameters() { - return minParameters; - } - public int getMaxParameters() { - return maxParameters; - } - - } - - /** - * A constant reference - e.g. a reference to a name that must be resolved in context. - * The % will be removed from the constant name before this is invoked. - * - * This will also be called if the host invokes the FluentPath engine with a context of null - * - * @param appContext - content passed into the fluent path engine - * @param name - name reference to resolve - * @return the value of the reference (or null, if it's not valid, though can throw an exception if desired) - */ - public Base resolveConstant(Object appContext, String name) throws PathEngineException; - public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException; - - /** - * when the .log() function is called - * - * @param argument - * @param focus - * @return - */ - public boolean log(String argument, List focus); - - // extensibility for functions - /** - * - * @param functionName - * @return null if the function is not known - */ - public FunctionDetails resolveFunction(String functionName); - - /** - * Check the function parameters, and throw an error if they are incorrect, or return the type for the function - * @param functionName - * @param parameters - * @return - */ - public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException; - - /** - * @param appContext - * @param functionName - * @param parameters - * @return - */ - public List executeFunction(Object appContext, String functionName, List> parameters); - - /** - * Implementation of resolve() function. Passed a string, return matching resource, if one is known - else null - * @param url - * @return - * @throws FHIRException - */ - public Base resolveReference(Object appContext, String url) throws FHIRException; - - } - - /** * @param worker - used when validating paths (@check), and used doing value set membership when executing tests (once that's defined) */ @@ -227,137 +50,84 @@ public class FHIRPathEngine { } } + private TypeDetails anything(CollectionStatus status) { + return new TypeDetails(status, allTypes.keySet()); + } + + private ExecutionContext changeThis(ExecutionContext context, Base newThis) { + return new ExecutionContext(context.appInfo, context.resource, context.context, context.aliases, newThis); + } + + private ExecutionTypeContext changeThis(ExecutionTypeContext context, TypeDetails newThis) { + return new ExecutionTypeContext(context.appInfo, context.resource, context.context, newThis); + } + // --- 3 methods to override in children ------------------------------------------------------- // if you don't override, it falls through to the using the base reference implementation // HAPI overrides to these to support extending the base model - public IEvaluationContext getHostServices() { - return hostServices; - } - - - public void setHostServices(IEvaluationContext constantResolver) { - this.hostServices = constantResolver; - } - - - /** - * Given an item, return all the children that conform to the pattern described in name - * - * Possible patterns: - * - a simple name (which may be the base of a name with [] e.g. value[x]) - * - a name with a type replacement e.g. valueCodeableConcept - * - * which means all children - * - ** which means all descendants - * - * @param item - * @param name - * @param result - * @throws FHIRException - */ - protected void getChildrenByName(Base item, String name, List result) throws FHIRException { - Base[] list = item.listChildrenByName(name, false); - if (list != null) - for (Base v : list) - if (v != null) - result.add(v); - } - - // --- public API ------------------------------------------------------- - /** - * Parse a path for later use using execute - * - * @param path - * @return - * @throws PathEngineException - * @throws Exception - */ - public ExpressionNode parse(String path) throws FHIRLexerException { - FHIRLexer lexer = new FHIRLexer(path); - if (lexer.done()) - throw lexer.error("Path cannot be empty"); - ExpressionNode result = parseExpression(lexer, true); - if (!lexer.done()) - throw lexer.error("Premature ExpressionNode termination at unexpected token \""+lexer.getCurrent()+"\""); - result.check(); - return result; - } - - /** - * Parse a path that is part of some other syntax - * - * @return - * @throws PathEngineException - * @throws Exception - */ - public ExpressionNode parse(FHIRLexer lexer) throws FHIRLexerException { - ExpressionNode result = parseExpression(lexer, true); - result.check(); - return result; - } - /** * check that paths referred to in the ExpressionNode are valid - * + *

    * xPathStartsWithValueRef is a hack work around for the fact that FHIR Path sometimes needs a different starting point than the xpath - * + *

    * returns a list of the possible types that might be returned by executing the ExpressionNode against a particular context - * + * * @param context - the logical type against which this path is applied * @throws DefinitionException - * @throws PathEngineException + * @throws PathEngineException * @if the path is not valid */ public TypeDetails check(Object appContext, String resourceType, String context, ExpressionNode expr) throws FHIRLexerException, PathEngineException, DefinitionException { - // if context is a path that refers to a type, do that conversion now - TypeDetails types; - if (context == null) { - types = null; // this is a special case; the first path reference will have to resolve to something in the context - } else if (!context.contains(".")) { - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, context); - types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl()); - } else { - String ctxt = context.substring(0, context.indexOf('.')); + // if context is a path that refers to a type, do that conversion now + TypeDetails types; + if (context == null) { + types = null; // this is a special case; the first path reference will have to resolve to something in the context + } else if (!context.contains(".")) { + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, context); + types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl()); + } else { + String ctxt = context.substring(0, context.indexOf('.')); if (Utilities.isAbsoluteUrl(resourceType)) { - ctxt = resourceType.substring(0, resourceType.lastIndexOf("/")+1)+ctxt; + ctxt = resourceType.substring(0, resourceType.lastIndexOf("/") + 1) + ctxt; } - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, ctxt); - if (sd == null) - throw new PathEngineException("Unknown context "+context); - ElementDefinitionMatch ed = getElementDefinition(sd, context, true); - if (ed == null) - throw new PathEngineException("Unknown context element "+context); - if (ed.fixedType != null) - types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); - else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) - types = new TypeDetails(CollectionStatus.SINGLETON, ctxt+"#"+context); - else { - types = new TypeDetails(CollectionStatus.SINGLETON); - for (TypeRefComponent t : ed.getDefinition().getType()) - types.addType(t.getCode()); - } - } + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, ctxt); + if (sd == null) + throw new PathEngineException("Unknown context " + context); + ElementDefinitionMatch ed = getElementDefinition(sd, context, true); + if (ed == null) + throw new PathEngineException("Unknown context element " + context); + if (ed.fixedType != null) + types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); + else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) + types = new TypeDetails(CollectionStatus.SINGLETON, ctxt + "#" + context); + else { + types = new TypeDetails(CollectionStatus.SINGLETON); + for (TypeRefComponent t : ed.getDefinition().getType()) + types.addType(t.getCode()); + } + } return executeType(new ExecutionTypeContext(appContext, resourceType, context, types), types, expr, true); } public TypeDetails check(Object appContext, StructureDefinition sd, String context, ExpressionNode expr) throws FHIRLexerException, PathEngineException, DefinitionException { - // if context is a path that refers to a type, do that conversion now - TypeDetails types; + // if context is a path that refers to a type, do that conversion now + TypeDetails types; if (!context.contains(".")) { types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl()); } else { ElementDefinitionMatch ed = getElementDefinition(sd, context, true); - if (ed == null) - throw new PathEngineException("Unknown context element "+context); - if (ed.fixedType != null) + if (ed == null) + throw new PathEngineException("Unknown context element " + context); + if (ed.fixedType != null) types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); - else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) - types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl()+"#"+context); + else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) + types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl() + "#" + context); else { types = new TypeDetails(CollectionStatus.SINGLETON); - for (TypeRefComponent t : ed.getDefinition().getType()) + for (TypeRefComponent t : ed.getDefinition().getType()) types.addType(t.getCode()); } } @@ -366,15 +136,218 @@ public class FHIRPathEngine { } public TypeDetails check(Object appContext, StructureDefinition sd, ExpressionNode expr) throws FHIRLexerException, PathEngineException, DefinitionException { - // if context is a path that refers to a type, do that conversion now + // if context is a path that refers to a type, do that conversion now TypeDetails types = null; // this is a special case; the first path reference will have to resolve to something in the context return executeType(new ExecutionTypeContext(appContext, sd == null ? null : sd.getUrl(), null, types), types, expr, true); } + // --- public API ------------------------------------------------------- + public TypeDetails check(Object appContext, String resourceType, String context, String expr) throws FHIRLexerException, PathEngineException, DefinitionException { return check(appContext, resourceType, context, parse(expr)); } + private void checkContextCoded(TypeDetails focus, String name) throws PathEngineException { + if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Coding") && !focus.hasType(worker, "CodeableConcept")) + throw new PathEngineException("The function '" + name + "'() can only be used on string, code, uri, Coding, CodeableConcept"); + } + + private void checkContextPrimitive(TypeDetails focus, String name, boolean canQty) throws PathEngineException { + if (canQty) { + if (!focus.hasType(primitiveTypes) && !focus.hasType("Quantity")) + throw new PathEngineException("The function '" + name + "'() can only be used on a Quantity or on " + primitiveTypes.toString()); + } else if (!focus.hasType(primitiveTypes)) + throw new PathEngineException("The function '" + name + "'() can only be used on " + primitiveTypes.toString()); + } + + private void checkContextReference(TypeDetails focus, String name) throws PathEngineException { + if (!focus.hasType(worker, "string") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Reference") && !focus.hasType(worker, "canonical")) + throw new PathEngineException("The function '" + name + "'() can only be used on string, uri, canonical, Reference"); + } + + private void checkContextString(TypeDetails focus, String name) throws PathEngineException { + if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "id")) + throw new PathEngineException("The function '" + name + "'() can only be used on string, uri, code, id, but found " + focus.describe()); + } + + private void checkOrdered(TypeDetails focus, String name) throws PathEngineException { + if (focus.getCollectionStatus() == CollectionStatus.UNORDERED) + throw new PathEngineException("The function '" + name + "'() can only be used on ordered collections"); + } + + private boolean checkParamCount(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, int count) throws FHIRLexerException { + if (exp.getParameters().size() != count) + throw lexer.error("The function \"" + exp.getName() + "\" requires " + Integer.toString(count) + " parameters", location.toString()); + return true; + } + + private boolean checkParamCount(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, int countMin, int countMax) throws FHIRLexerException { + if (exp.getParameters().size() < countMin || exp.getParameters().size() > countMax) + throw lexer.error("The function \"" + exp.getName() + "\" requires between " + Integer.toString(countMin) + " and " + Integer.toString(countMax) + " parameters", location.toString()); + return true; + } + + private void checkParamTypes(String funcName, List paramTypes, TypeDetails... typeSet) throws PathEngineException { + int i = 0; + for (TypeDetails pt : typeSet) { + if (i == paramTypes.size()) + return; + TypeDetails actual = paramTypes.get(i); + i++; + for (String a : actual.getTypes()) { + if (!pt.hasType(worker, a)) + throw new PathEngineException("The parameter type '" + a + "' is not legal for " + funcName + " parameter " + Integer.toString(i) + ". expecting " + pt.toString()); + } + } + } + + private boolean checkParameters(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, FunctionDetails details) throws FHIRLexerException { + switch (exp.getFunction()) { + case Empty: + return checkParamCount(lexer, location, exp, 0); + case Not: + return checkParamCount(lexer, location, exp, 0); + case Exists: + return checkParamCount(lexer, location, exp, 0); + case SubsetOf: + return checkParamCount(lexer, location, exp, 1); + case SupersetOf: + return checkParamCount(lexer, location, exp, 1); + case IsDistinct: + return checkParamCount(lexer, location, exp, 0); + case Distinct: + return checkParamCount(lexer, location, exp, 0); + case Count: + return checkParamCount(lexer, location, exp, 0); + case Where: + return checkParamCount(lexer, location, exp, 1); + case Select: + return checkParamCount(lexer, location, exp, 1); + case All: + return checkParamCount(lexer, location, exp, 0, 1); + case Repeat: + return checkParamCount(lexer, location, exp, 1); + case Aggregate: + return checkParamCount(lexer, location, exp, 1, 2); + case Item: + return checkParamCount(lexer, location, exp, 1); + case As: + return checkParamCount(lexer, location, exp, 1); + case OfType: + return checkParamCount(lexer, location, exp, 1); + case Type: + return checkParamCount(lexer, location, exp, 0); + case Is: + return checkParamCount(lexer, location, exp, 1); + case Single: + return checkParamCount(lexer, location, exp, 0); + case First: + return checkParamCount(lexer, location, exp, 0); + case Last: + return checkParamCount(lexer, location, exp, 0); + case Tail: + return checkParamCount(lexer, location, exp, 0); + case Skip: + return checkParamCount(lexer, location, exp, 1); + case Take: + return checkParamCount(lexer, location, exp, 1); + case Union: + return checkParamCount(lexer, location, exp, 1); + case Combine: + return checkParamCount(lexer, location, exp, 1); + case Intersect: + return checkParamCount(lexer, location, exp, 1); + case Exclude: + return checkParamCount(lexer, location, exp, 1); + case Iif: + return checkParamCount(lexer, location, exp, 2, 3); + case Lower: + return checkParamCount(lexer, location, exp, 0); + case Upper: + return checkParamCount(lexer, location, exp, 0); + case ToChars: + return checkParamCount(lexer, location, exp, 0); + case Substring: + return checkParamCount(lexer, location, exp, 1, 2); + case StartsWith: + return checkParamCount(lexer, location, exp, 1); + case EndsWith: + return checkParamCount(lexer, location, exp, 1); + case Matches: + return checkParamCount(lexer, location, exp, 1); + case ReplaceMatches: + return checkParamCount(lexer, location, exp, 2); + case Contains: + return checkParamCount(lexer, location, exp, 1); + case Replace: + return checkParamCount(lexer, location, exp, 2); + case Length: + return checkParamCount(lexer, location, exp, 0); + case Children: + return checkParamCount(lexer, location, exp, 0); + case Descendants: + return checkParamCount(lexer, location, exp, 0); + case MemberOf: + return checkParamCount(lexer, location, exp, 1); + case Trace: + return checkParamCount(lexer, location, exp, 1); + case Today: + return checkParamCount(lexer, location, exp, 0); + case Now: + return checkParamCount(lexer, location, exp, 0); + case Resolve: + return checkParamCount(lexer, location, exp, 0); + case Extension: + return checkParamCount(lexer, location, exp, 1); + case HasValue: + return checkParamCount(lexer, location, exp, 0); + case Alias: + return checkParamCount(lexer, location, exp, 1); + case AliasAs: + return checkParamCount(lexer, location, exp, 1); + case HtmlChecks: + return checkParamCount(lexer, location, exp, 0); + case ToInteger: + return checkParamCount(lexer, location, exp, 0); + case ToDecimal: + return checkParamCount(lexer, location, exp, 0); + case ToString: + return checkParamCount(lexer, location, exp, 0); + case ToQuantity: + return checkParamCount(lexer, location, exp, 0); + case ToBoolean: + return checkParamCount(lexer, location, exp, 0); + case ToDateTime: + return checkParamCount(lexer, location, exp, 0); + case ToTime: + return checkParamCount(lexer, location, exp, 0); + case IsInteger: + return checkParamCount(lexer, location, exp, 0); + case IsDecimal: + return checkParamCount(lexer, location, exp, 0); + case IsString: + return checkParamCount(lexer, location, exp, 0); + case IsQuantity: + return checkParamCount(lexer, location, exp, 0); + case IsBoolean: + return checkParamCount(lexer, location, exp, 0); + case IsDateTime: + return checkParamCount(lexer, location, exp, 0); + case IsTime: + return checkParamCount(lexer, location, exp, 0); + case Custom: + return checkParamCount(lexer, location, exp, details.getMinParameters(), details.getMaxParameters()); + } + return false; + } + + private TypeDetails childTypes(TypeDetails focus, String mask) throws PathEngineException, DefinitionException { + TypeDetails result = new TypeDetails(CollectionStatus.UNORDERED); + for (String f : focus.getTypes()) + getChildTypesByName(f, mask, result); + return result; + } + private int compareDateTimeElements(Base theL, Base theR, boolean theEquivalenceTest) { String dateLeftString = theL.primitiveValue(); DateTimeType dateLeft = new DateTimeType(dateLeftString); @@ -402,162 +375,25 @@ public class FHIRPathEngine { } /** - * evaluate a path and return the matching elements - * - * @param base - the object against which the path is being evaluated - * @param ExpressionNode - the parsed ExpressionNode statement to use + * worker routine for converting a set of objects to a boolean representation (for invariants) + * + * @param items - result from @evaluate * @return - * @throws FHIRException - * @ */ - public List evaluate(Base base, ExpressionNode ExpressionNode) throws FHIRException { - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(null, base != null && base.isResource() ? base : null, base, null, base), list, ExpressionNode, true); - } - - /** - * evaluate a path and return the matching elements - * - * @param base - the object against which the path is being evaluated - * @param path - the FHIR Path statement to use - * @return - * @throws FHIRException - * @ - */ - public List evaluate(Base base, String path) throws FHIRException { - ExpressionNode exp = parse(path); - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(null, base.isResource() ? base : null, base, null, base), list, exp, true); - } - - /** - * evaluate a path and return the matching elements - * - * @param base - the object against which the path is being evaluated - * @param ExpressionNode - the parsed ExpressionNode statement to use - * @return - * @throws FHIRException - * @ - */ - public List evaluate(Object appContext, Resource resource, Base base, ExpressionNode ExpressionNode) throws FHIRException { - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(appContext, resource, base, null, base), list, ExpressionNode, true); - } - - /** - * evaluate a path and return the matching elements - * - * @param base - the object against which the path is being evaluated - * @param ExpressionNode - the parsed ExpressionNode statement to use - * @return - * @throws FHIRException - * @ - */ - public List evaluate(Object appContext, Base resource, Base base, ExpressionNode ExpressionNode) throws FHIRException { - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(appContext, resource, base, null, base), list, ExpressionNode, true); - } - - /** - * evaluate a path and return the matching elements - * - * @param base - the object against which the path is being evaluated - * @param path - the FHIR Path statement to use - * @return - * @throws FHIRException - * @ - */ - public List evaluate(Object appContext, Resource resource, Base base, String path) throws FHIRException { - ExpressionNode exp = parse(path); - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(appContext, resource, base, null, base), list, exp, true); - } - - /** - * evaluate a path and return true or false (e.g. for an invariant) - * - * @param base - the object against which the path is being evaluated - * @param path - the FHIR Path statement to use - * @return - * @throws FHIRException - * @ - */ - public boolean evaluateToBoolean(Resource resource, Base base, String path) throws FHIRException { - return convertToBoolean(evaluate(null, resource, base, path)); - } - - /** - * evaluate a path and return true or false (e.g. for an invariant) - * - * @param base - the object against which the path is being evaluated - * @return - * @throws FHIRException - * @ - */ - public boolean evaluateToBoolean(Resource resource, Base base, ExpressionNode node) throws FHIRException { - return convertToBoolean(evaluate(null, resource, base, node)); - } - - /** - * evaluate a path and return true or false (e.g. for an invariant) - * - * @param appInfo - application context - * @param base - the object against which the path is being evaluated - * @return - * @throws FHIRException - * @ - */ - public boolean evaluateToBoolean(Object appInfo, Base resource, Base base, ExpressionNode node) throws FHIRException { - return convertToBoolean(evaluate(appInfo, resource, base, node)); - } - - /** - * evaluate a path and return true or false (e.g. for an invariant) - * - * @param base - the object against which the path is being evaluated - * @return - * @throws FHIRException - * @ - */ - public boolean evaluateToBoolean(Base resource, Base base, ExpressionNode node) throws FHIRException { - return convertToBoolean(evaluate(null, resource, base, node)); - } - - /** - * evaluate a path and a string containing the outcome (for display) - * - * @param base - the object against which the path is being evaluated - * @param path - the FHIR Path statement to use - * @return - * @throws FHIRException - * @ - */ - public String evaluateToString(Base base, String path) throws FHIRException { - return convertToString(evaluate(base, path)); - } - - public String evaluateToString(Object appInfo, Base resource, Base base, ExpressionNode node) throws FHIRException { - return convertToString(evaluate(appInfo, resource, base, node)); + public boolean convertToBoolean(List items) { + if (items == null) + return false; + else if (items.size() == 1 && items.get(0) instanceof BooleanType) + return ((BooleanType) items.get(0)).getValue(); + else if (items.size() == 1 && items.get(0).isBooleanPrimitive()) // element model + return Boolean.valueOf(items.get(0).primitiveValue()); + else + return items.size() > 0; } /** * worker routine for converting a set of objects to a string representation - * + * * @param items - result from @evaluate * @return */ @@ -565,7 +401,7 @@ public class FHIRPathEngine { StringBuilder b = new StringBuilder(); boolean first = true; for (Base item : items) { - if (first) + if (first) first = false; else b.append(','); @@ -581,7 +417,7 @@ public class FHIRPathEngine { else if (item instanceof Quantity) { Quantity q = (Quantity) item; if (q.getSystem().equals("http://unitsofmeasure.org")) { - String u = "'"+q.getCode()+"'"; + String u = "'" + q.getCode() + "'"; boolean plural = !q.getValue().toPlainString().equals("1"); if ("a".equals(q.getCode())) u = plural ? "years" : "year"; @@ -599,257 +435,1689 @@ public class FHIRPathEngine { u = plural ? "seconds" : "seconds"; else if ("ms".equals(q.getCode())) u = plural ? "milliseconds" : "milliseconds"; - return q.getValue().toPlainString()+" "+u; - } - else + return q.getValue().toPlainString() + " " + u; + } else return item.toString(); } else return item.toString(); } - /** - * worker routine for converting a set of objects to a boolean representation (for invariants) - * - * @param items - result from @evaluate - * @return - */ - public boolean convertToBoolean(List items) { - if (items == null) - return false; - else if (items.size() == 1 && items.get(0) instanceof BooleanType) - return ((BooleanType) items.get(0)).getValue(); - else if (items.size() == 1 && items.get(0).isBooleanPrimitive()) // element model - return Boolean.valueOf(items.get(0).primitiveValue()); - else - return items.size() > 0; + private boolean doContains(List list, Base item) { + for (Base test : list) + if (doEquals(test, item)) + return true; + return false; } - - private void log(String name, List contents) { - if (hostServices == null || !hostServices.log(name, contents)) { - if (log.length() > 0) - log.append("; "); - log.append(name); - log.append(": "); - boolean first = true; - for (Base b : contents) { - if (first) - first = false; - else - log.append(","); - log.append(convertToString(b)); - } - } - } - - public String forLog() { - if (log.length() > 0) - return " ("+log.toString()+")"; + private boolean doEquals(Base left, Base right) { + if (left instanceof Quantity && right instanceof Quantity) + return qtyEqual((Quantity) left, (Quantity) right); + else if (left.isPrimitive() && right.isPrimitive()) + return Base.equals(left.primitiveValue(), right.primitiveValue()); else - return ""; + return Base.compareDeep(left, right, false); } - private class ExecutionContext { - private Object appInfo; - private Base resource; - private Base context; - private Base thisItem; - private List total; - private Map aliases; - - public ExecutionContext(Object appInfo, Base resource, Base context, Map aliases, Base thisItem) { - this.appInfo = appInfo; - this.context = context; - this.resource = resource; - this.aliases = aliases; - this.thisItem = thisItem; - } - public Base getResource() { - return resource; - } - public Base getThisItem() { - return thisItem; - } - public List getTotal() { - return total; - } - public void addAlias(String name, List focus) throws FHIRException { - if (aliases == null) - aliases = new HashMap(); - else - aliases = new HashMap(aliases); // clone it, since it's going to change - if (focus.size() > 1) - throw new FHIRException("Attempt to alias a collection, not a singleton"); - aliases.put(name, focus.size() == 0 ? null : focus.get(0)); - } - public Base getAlias(String name) { - return aliases == null ? null : aliases.get(name); - } + private boolean doEquivalent(Base left, Base right) throws PathEngineException { + if (left instanceof Quantity && right instanceof Quantity) + return qtyEquivalent((Quantity) left, (Quantity) right); + if (left.hasType("integer") && right.hasType("integer")) + return doEquals(left, right); + if (left.hasType("boolean") && right.hasType("boolean")) + return doEquals(left, right); + if (left.hasType("integer", "decimal", "unsignedInt", "positiveInt") && right.hasType("integer", "decimal", "unsignedInt", "positiveInt")) + return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); + if (left.hasType("date", "dateTime", "time", "instant") && right.hasType("date", "dateTime", "time", "instant")) + return compareDateTimeElements(left, right, true) == 0; + if (left.hasType("string", "id", "code", "uri") && right.hasType("string", "id", "code", "uri")) + return Utilities.equivalent(convertToString(left), convertToString(right)); + + throw new PathEngineException(String.format("Unable to determine equivalence between %s and %s", left.fhirType(), right.fhirType())); } - private class ExecutionTypeContext { - private Object appInfo; - private String resource; - private String context; - private TypeDetails thisItem; - private TypeDetails total; - - - public ExecutionTypeContext(Object appInfo, String resource, String context, TypeDetails thisItem) { - super(); - this.appInfo = appInfo; - this.resource = resource; - this.context = context; - this.thisItem = thisItem; - - } - public String getResource() { - return resource; - } - public TypeDetails getThisItem() { - return thisItem; - } - - + /** + * evaluate a path and return the matching elements + * + * @param base - the object against which the path is being evaluated + * @param ExpressionNode - the parsed ExpressionNode statement to use + * @return + * @throws FHIRException + * @ + */ + public List evaluate(Base base, ExpressionNode ExpressionNode) throws FHIRException { + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(null, base != null && base.isResource() ? base : null, base, null, base), list, ExpressionNode, true); } - private ExpressionNode parseExpression(FHIRLexer lexer, boolean proximal) throws FHIRLexerException { - ExpressionNode result = new ExpressionNode(lexer.nextId()); - SourceLocation c = lexer.getCurrentStartLocation(); - result.setStart(lexer.getCurrentLocation()); - // special: - if (lexer.getCurrent().equals("-")) { - lexer.take(); - lexer.setCurrent("-"+lexer.getCurrent()); - } - if (lexer.getCurrent().equals("+")) { - lexer.take(); - lexer.setCurrent("+"+lexer.getCurrent()); - } - if (lexer.isConstant(false)) { - boolean isString = lexer.isStringConstant(); - result.setConstant(processConstant(lexer)); - result.setKind(Kind.Constant); - if (!isString && !lexer.done() && (result.getConstant() instanceof IntegerType || result.getConstant() instanceof DecimalType) && (lexer.isStringConstant() || lexer.hasToken("year", "years", "month", "months", "week", "weeks", "day", "days", "hour", "hours", "minute", "minutes", "second", "seconds", "millisecond", "milliseconds"))) { - // it's a quantity - String ucum = null; - if (lexer.hasToken("year", "years", "month", "months", "week", "weeks", "day", "days", "hour", "hours", "minute", "minutes", "second", "seconds", "millisecond", "milliseconds")) { - String s = lexer.take(); - if (s.equals("year") || s.equals("years")) - ucum = "a"; - else if (s.equals("month") || s.equals("months")) - ucum = "mo"; - else if (s.equals("week") || s.equals("weeks")) - ucum = "wk"; - else if (s.equals("day") || s.equals("days")) - ucum = "d"; - else if (s.equals("hour") || s.equals("hours")) - ucum = "h"; - else if (s.equals("minute") || s.equals("minutes")) - ucum = "min"; - else if (s.equals("second") || s.equals("seconds")) - ucum = "s"; - else // (s.equals("millisecond") || s.equals("milliseconds")) - ucum = "ms"; - } else - ucum = lexer.readConstant("units"); - result.setConstant(new Quantity().setValue(new BigDecimal(result.getConstant().primitiveValue())).setSystem("http://unitsofmeasure.org").setCode(ucum)); + /** + * evaluate a path and return the matching elements + * + * @param base - the object against which the path is being evaluated + * @param path - the FHIR Path statement to use + * @return + * @throws FHIRException + * @ + */ + public List evaluate(Base base, String path) throws FHIRException { + ExpressionNode exp = parse(path); + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(null, base.isResource() ? base : null, base, null, base), list, exp, true); + } + + /** + * evaluate a path and return the matching elements + * + * @param base - the object against which the path is being evaluated + * @param ExpressionNode - the parsed ExpressionNode statement to use + * @return + * @throws FHIRException + * @ + */ + public List evaluate(Object appContext, Resource resource, Base base, ExpressionNode ExpressionNode) throws FHIRException { + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(appContext, resource, base, null, base), list, ExpressionNode, true); + } + + /** + * evaluate a path and return the matching elements + * + * @param base - the object against which the path is being evaluated + * @param ExpressionNode - the parsed ExpressionNode statement to use + * @return + * @throws FHIRException + * @ + */ + public List evaluate(Object appContext, Base resource, Base base, ExpressionNode ExpressionNode) throws FHIRException { + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(appContext, resource, base, null, base), list, ExpressionNode, true); + } + + /** + * evaluate a path and return the matching elements + * + * @param base - the object against which the path is being evaluated + * @param path - the FHIR Path statement to use + * @return + * @throws FHIRException + * @ + */ + public List evaluate(Object appContext, Resource resource, Base base, String path) throws FHIRException { + ExpressionNode exp = parse(path); + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(appContext, resource, base, null, base), list, exp, true); + } + + /** + * given an element definition in a profile, what element contains the differentiating fixed + * for the element, given the differentiating expresssion. The expression is only allowed to + * use a subset of FHIRPath + * + * @param profile + * @param element + * @return + * @throws PathEngineException + * @throws DefinitionException + */ + public ElementDefinition evaluateDefinition(ExpressionNode expr, StructureDefinition profile, ElementDefinition element) throws DefinitionException { + StructureDefinition sd = profile; + ElementDefinition focus = null; + + if (expr.getKind() == Kind.Name) { + List childDefinitions; + childDefinitions = ProfileUtilities.getChildMap(sd, element); + // if that's empty, get the children of the type + if (childDefinitions.isEmpty()) { + sd = fetchStructureByType(element); + if (sd == null) + throw new DefinitionException("Problem with use of resolve() - profile '" + element.getType().get(0).getProfile() + "' on " + element.getId() + " could not be resolved"); + childDefinitions = ProfileUtilities.getChildMap(sd, sd.getSnapshot().getElementFirstRep()); } - result.setEnd(lexer.getCurrentLocation()); - } else if ("(".equals(lexer.getCurrent())) { - lexer.next(); - result.setKind(Kind.Group); - result.setGroup(parseExpression(lexer, true)); - if (!")".equals(lexer.getCurrent())) - throw lexer.error("Found "+lexer.getCurrent()+" expecting a \")\""); - result.setEnd(lexer.getCurrentLocation()); - lexer.next(); - } else { - if (!lexer.isToken() && !lexer.getCurrent().startsWith("\"")) - throw lexer.error("Found "+lexer.getCurrent()+" expecting a token name"); - if (lexer.getCurrent().startsWith("\"")) - result.setName(lexer.readConstant("Path Name")); - else - result.setName(lexer.take()); - result.setEnd(lexer.getCurrentLocation()); - if (!result.checkName()) - throw lexer.error("Found "+result.getName()+" expecting a valid token name"); - if ("(".equals(lexer.getCurrent())) { - Function f = Function.fromCode(result.getName()); - FunctionDetails details = null; - if (f == null) { - if (hostServices != null) - details = hostServices.resolveFunction(result.getName()); - if (details == null) - throw lexer.error("The name "+result.getName()+" is not a valid function name"); - f = Function.Custom; + for (ElementDefinition t : childDefinitions) { + if (tailMatches(t, expr.getName())) { + focus = t; + break; } - result.setKind(Kind.Function); - result.setFunction(f); - lexer.next(); - while (!")".equals(lexer.getCurrent())) { - result.getParameters().add(parseExpression(lexer, true)); - if (",".equals(lexer.getCurrent())) - lexer.next(); - else if (!")".equals(lexer.getCurrent())) - throw lexer.error("The token "+lexer.getCurrent()+" is not expected here - either a \",\" or a \")\" expected"); + } + } else if (expr.getKind() == Kind.Function) { + if ("resolve".equals(expr.getName())) { + if (!element.hasType()) + throw new DefinitionException("illegal use of resolve() in discriminator - no type on element " + element.getId()); + if (element.getType().size() > 1) + throw new DefinitionException("illegal use of resolve() in discriminator - Multiple possible types on " + element.getId()); + if (!element.getType().get(0).hasTarget()) + throw new DefinitionException("illegal use of resolve() in discriminator - type on " + element.getId() + " is not Reference (" + element.getType().get(0).getCode() + ")"); + if (element.getType().get(0).getTargetProfile().size() > 1) + throw new DefinitionException("illegal use of resolve() in discriminator - Multiple possible target type profiles on " + element.getId()); + sd = worker.fetchResource(StructureDefinition.class, element.getType().get(0).getTargetProfile().get(0).getValue()); + if (sd == null) + throw new DefinitionException("Problem with use of resolve() - profile '" + element.getType().get(0).getTargetProfile() + "' on " + element.getId() + " could not be resolved"); + focus = sd.getSnapshot().getElementFirstRep(); + } else if ("extension".equals(expr.getName())) { + String targetUrl = expr.getParameters().get(0).getConstant().primitiveValue(); +// targetUrl = targetUrl.substring(1,targetUrl.length()-1); + List childDefinitions = ProfileUtilities.getChildMap(sd, element); + for (ElementDefinition t : childDefinitions) { + if (t.getPath().endsWith(".extension") && t.hasSliceName()) { + sd = worker.fetchResource(StructureDefinition.class, t.getType().get(0).getProfile().get(0).getValue()); + while (sd != null && !sd.getBaseDefinition().equals("http://hl7.org/fhir/StructureDefinition/Extension")) + sd = worker.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); + if (sd.getUrl().equals(targetUrl)) { + focus = t; + break; + } + } } - result.setEnd(lexer.getCurrentLocation()); - lexer.next(); - checkParameters(lexer, c, result, details); } else - result.setKind(Kind.Name); + throw new DefinitionException("illegal function name " + expr.getName() + "() in discriminator"); + } else if (expr.getKind() == Kind.Group) { + throw new DefinitionException("illegal expression syntax in discriminator (group)"); + } else if (expr.getKind() == Kind.Constant) { + throw new DefinitionException("illegal expression syntax in discriminator (const)"); } - ExpressionNode focus = result; - if ("[".equals(lexer.getCurrent())) { - lexer.next(); - ExpressionNode item = new ExpressionNode(lexer.nextId()); - item.setKind(Kind.Function); - item.setFunction(ExpressionNode.Function.Item); - item.getParameters().add(parseExpression(lexer, true)); - if (!lexer.getCurrent().equals("]")) - throw lexer.error("The token "+lexer.getCurrent()+" is not expected here - a \"]\" expected"); - lexer.next(); - result.setInner(item); - focus = item; - } - if (".".equals(lexer.getCurrent())) { - lexer.next(); - focus.setInner(parseExpression(lexer, false)); - } - result.setProximal(proximal); - if (proximal) { - while (lexer.isOp()) { - focus.setOperation(ExpressionNode.Operation.fromCode(lexer.getCurrent())); - focus.setOpStart(lexer.getCurrentStartLocation()); - focus.setOpEnd(lexer.getCurrentLocation()); - lexer.next(); - focus.setOpNext(parseExpression(lexer, false)); - focus = focus.getOpNext(); + + if (focus == null) + throw new DefinitionException("Unable to resolve discriminator"); + else if (expr.getInner() == null) + return focus; + else + return evaluateDefinition(expr.getInner(), sd, focus); + } + + private List evaluateFunction(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + switch (exp.getFunction()) { + case Empty: + return funcEmpty(context, focus, exp); + case Not: + return funcNot(context, focus, exp); + case Exists: + return funcExists(context, focus, exp); + case SubsetOf: + return funcSubsetOf(context, focus, exp); + case SupersetOf: + return funcSupersetOf(context, focus, exp); + case IsDistinct: + return funcIsDistinct(context, focus, exp); + case Distinct: + return funcDistinct(context, focus, exp); + case Count: + return funcCount(context, focus, exp); + case Where: + return funcWhere(context, focus, exp); + case Select: + return funcSelect(context, focus, exp); + case All: + return funcAll(context, focus, exp); + case Repeat: + return funcRepeat(context, focus, exp); + case Aggregate: + return funcAggregate(context, focus, exp); + case Item: + return funcItem(context, focus, exp); + case As: + return funcAs(context, focus, exp); + case OfType: + return funcAs(context, focus, exp); + case Type: + return funcType(context, focus, exp); + case Is: + return funcIs(context, focus, exp); + case Single: + return funcSingle(context, focus, exp); + case First: + return funcFirst(context, focus, exp); + case Last: + return funcLast(context, focus, exp); + case Tail: + return funcTail(context, focus, exp); + case Skip: + return funcSkip(context, focus, exp); + case Take: + return funcTake(context, focus, exp); + case Union: + return funcUnion(context, focus, exp); + case Combine: + return funcCombine(context, focus, exp); + case Intersect: + return funcIntersect(context, focus, exp); + case Exclude: + return funcExclude(context, focus, exp); + case Iif: + return funcIif(context, focus, exp); + case Lower: + return funcLower(context, focus, exp); + case Upper: + return funcUpper(context, focus, exp); + case ToChars: + return funcToChars(context, focus, exp); + case Substring: + return funcSubstring(context, focus, exp); + case StartsWith: + return funcStartsWith(context, focus, exp); + case EndsWith: + return funcEndsWith(context, focus, exp); + case Matches: + return funcMatches(context, focus, exp); + case ReplaceMatches: + return funcReplaceMatches(context, focus, exp); + case Contains: + return funcContains(context, focus, exp); + case Replace: + return funcReplace(context, focus, exp); + case Length: + return funcLength(context, focus, exp); + case Children: + return funcChildren(context, focus, exp); + case Descendants: + return funcDescendants(context, focus, exp); + case MemberOf: + return funcMemberOf(context, focus, exp); + case Trace: + return funcTrace(context, focus, exp); + case Today: + return funcToday(context, focus, exp); + case Now: + return funcNow(context, focus, exp); + case Resolve: + return funcResolve(context, focus, exp); + case Extension: + return funcExtension(context, focus, exp); + case HasValue: + return funcHasValue(context, focus, exp); + case AliasAs: + return funcAliasAs(context, focus, exp); + case Alias: + return funcAlias(context, focus, exp); + case HtmlChecks: + return funcHtmlChecks(context, focus, exp); + case ToInteger: + return funcToInteger(context, focus, exp); + case ToDecimal: + return funcToDecimal(context, focus, exp); + case ToString: + return funcToString(context, focus, exp); + case ToBoolean: + return funcToBoolean(context, focus, exp); + case ToQuantity: + return funcToQuantity(context, focus, exp); + case ToDateTime: + return funcToDateTime(context, focus, exp); + case ToTime: + return funcToTime(context, focus, exp); + case IsInteger: + return funcIsInteger(context, focus, exp); + case IsDecimal: + return funcIsDecimal(context, focus, exp); + case IsString: + return funcIsString(context, focus, exp); + case IsBoolean: + return funcIsBoolean(context, focus, exp); + case IsQuantity: + return funcIsQuantity(context, focus, exp); + case IsDateTime: + return funcIsDateTime(context, focus, exp); + case IsTime: + return funcIsTime(context, focus, exp); + case Custom: { + List> params = new ArrayList>(); + for (ExpressionNode p : exp.getParameters()) + params.add(execute(context, focus, p, true)); + return hostServices.executeFunction(context.appInfo, exp.getName(), params); + } + default: + throw new Error("not Implemented yet"); + } + } + + @SuppressWarnings("unchecked") + private TypeDetails evaluateFunctionType(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp) throws PathEngineException, DefinitionException { + List paramTypes = new ArrayList(); + if (exp.getFunction() == Function.Is || exp.getFunction() == Function.As) + paramTypes.add(new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + else + for (ExpressionNode expr : exp.getParameters()) { + if (exp.getFunction() == Function.Where || exp.getFunction() == Function.Select || exp.getFunction() == Function.Repeat || exp.getFunction() == Function.Aggregate) + paramTypes.add(executeType(changeThis(context, focus), focus, expr, true)); + else + paramTypes.add(executeType(context, focus, expr, true)); + } + switch (exp.getFunction()) { + case Empty: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Not: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Exists: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case SubsetOf: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case SupersetOf: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case IsDistinct: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Distinct: + return focus; + case Count: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); + case Where: + return focus; + case Select: + return anything(focus.getCollectionStatus()); + case All: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Repeat: + return anything(focus.getCollectionStatus()); + case Aggregate: + return anything(focus.getCollectionStatus()); + case Item: { + checkOrdered(focus, "item"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); + return focus; + } + case As: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, exp.getParameters().get(0).getName()); + } + case OfType: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, exp.getParameters().get(0).getName()); + } + case Type: { + boolean s = false; + boolean c = false; + for (ProfiledType pt : focus.getProfiledTypes()) { + s = s || pt.isSystemType(); + c = c || !pt.isSystemType(); + } + if (s && c) + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_SimpleTypeInfo, TypeDetails.FP_ClassInfo); + else if (s) + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_SimpleTypeInfo); + else + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_ClassInfo); + } + case Is: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case Single: + return focus.toSingleton(); + case First: { + checkOrdered(focus, "first"); + return focus.toSingleton(); + } + case Last: { + checkOrdered(focus, "last"); + return focus.toSingleton(); + } + case Tail: { + checkOrdered(focus, "tail"); + return focus; + } + case Skip: { + checkOrdered(focus, "skip"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); + return focus; + } + case Take: { + checkOrdered(focus, "take"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); + return focus; + } + case Union: { + return focus.union(paramTypes.get(0)); + } + case Combine: { + return focus.union(paramTypes.get(0)); + } + case Intersect: { + return focus.intersect(paramTypes.get(0)); + } + case Exclude: { + return focus; + } + case Iif: { + TypeDetails types = new TypeDetails(null); + types.update(paramTypes.get(0)); + if (paramTypes.size() > 1) + types.update(paramTypes.get(1)); + return types; + } + case Lower: { + checkContextString(focus, "lower"); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); + } + case Upper: { + checkContextString(focus, "upper"); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); + } + case ToChars: { + checkContextString(focus, "toChars"); + return new TypeDetails(CollectionStatus.ORDERED, TypeDetails.FP_String); + } + case Substring: { + checkContextString(focus, "subString"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); + } + case StartsWith: { + checkContextString(focus, "startsWith"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case EndsWith: { + checkContextString(focus, "endsWith"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case Matches: { + checkContextString(focus, "matches"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case ReplaceMatches: { + checkContextString(focus, "replaceMatches"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); + } + case Contains: { + checkContextString(focus, "contains"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case Replace: { + checkContextString(focus, "replace"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); + } + case Length: { + checkContextPrimitive(focus, "length", false); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); + } + case Children: + return childTypes(focus, "*"); + case Descendants: + return childTypes(focus, "**"); + case MemberOf: { + checkContextCoded(focus, "memberOf"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case Trace: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return focus; + } + case Today: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); + case Now: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); + case Resolve: { + checkContextReference(focus, "resolve"); + return new TypeDetails(CollectionStatus.SINGLETON, "DomainResource"); + } + case Extension: { + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return new TypeDetails(CollectionStatus.SINGLETON, "Extension"); + } + case HasValue: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case HtmlChecks: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Alias: + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return anything(CollectionStatus.SINGLETON); + case AliasAs: + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); + return focus; + case ToInteger: { + checkContextPrimitive(focus, "toInteger", true); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); + } + case ToDecimal: { + checkContextPrimitive(focus, "toDecimal", true); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal); + } + case ToString: { + checkContextPrimitive(focus, "toString", true); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); + } + case ToQuantity: { + checkContextPrimitive(focus, "toQuantity", true); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Quantity); + } + case ToBoolean: { + checkContextPrimitive(focus, "toBoolean", false); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case ToDateTime: { + checkContextPrimitive(focus, "toBoolean", false); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); + } + case ToTime: { + checkContextPrimitive(focus, "toBoolean", false); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Time); + } + case IsString: + case IsQuantity: { + checkContextPrimitive(focus, exp.getFunction().toCode(), true); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case IsInteger: + case IsDecimal: + case IsDateTime: + case IsTime: + case IsBoolean: { + checkContextPrimitive(focus, exp.getFunction().toCode(), false); + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + } + case Custom: { + return hostServices.checkFunction(context.appInfo, exp.getName(), paramTypes); + } + default: + break; + } + throw new Error("not Implemented yet"); + } + + /** + * evaluate a path and return true or false (e.g. for an invariant) + * + * @param base - the object against which the path is being evaluated + * @param path - the FHIR Path statement to use + * @return + * @throws FHIRException + * @ + */ + public boolean evaluateToBoolean(Resource resource, Base base, String path) throws FHIRException { + return convertToBoolean(evaluate(null, resource, base, path)); + } + + /** + * evaluate a path and return true or false (e.g. for an invariant) + * + * @param base - the object against which the path is being evaluated + * @return + * @throws FHIRException + * @ + */ + public boolean evaluateToBoolean(Resource resource, Base base, ExpressionNode node) throws FHIRException { + return convertToBoolean(evaluate(null, resource, base, node)); + } + + /** + * evaluate a path and return true or false (e.g. for an invariant) + * + * @param appInfo - application context + * @param base - the object against which the path is being evaluated + * @return + * @throws FHIRException + * @ + */ + public boolean evaluateToBoolean(Object appInfo, Base resource, Base base, ExpressionNode node) throws FHIRException { + return convertToBoolean(evaluate(appInfo, resource, base, node)); + } + + /** + * evaluate a path and return true or false (e.g. for an invariant) + * + * @param base - the object against which the path is being evaluated + * @return + * @throws FHIRException + * @ + */ + public boolean evaluateToBoolean(Base resource, Base base, ExpressionNode node) throws FHIRException { + return convertToBoolean(evaluate(null, resource, base, node)); + } + + // procedure CheckParamCount(c : integer); + // begin + // if exp.Parameters.Count <> c then + // raise lexer.error('The function "'+exp.name+'" requires '+inttostr(c)+' parameters', offset); + // end; + + /** + * evaluate a path and a string containing the outcome (for display) + * + * @param base - the object against which the path is being evaluated + * @param path - the FHIR Path statement to use + * @return + * @throws FHIRException + * @ + */ + public String evaluateToString(Base base, String path) throws FHIRException { + return convertToString(evaluate(base, path)); + } + + public String evaluateToString(Object appInfo, Base resource, Base base, ExpressionNode node) throws FHIRException { + return convertToString(evaluate(appInfo, resource, base, node)); + } + + private List execute(ExecutionContext context, List focus, ExpressionNode exp, boolean atEntry) throws FHIRException { +// System.out.println("Evaluate {'"+exp.toString()+"'} on "+focus.toString()); + List work = new ArrayList(); + switch (exp.getKind()) { + case Name: + if (atEntry && exp.getName().equals("$this")) + work.add(context.getThisItem()); + else if (atEntry && exp.getName().equals("$total")) + work.addAll(context.getTotal()); + else + for (Base item : focus) { + List outcome = execute(context, item, exp, atEntry); + for (Base base : outcome) + if (base != null) + work.add(base); + } + break; + case Function: + List work2 = evaluateFunction(context, focus, exp); + work.addAll(work2); + break; + case Constant: + Base b = resolveConstant(context, exp.getConstant()); + if (b != null) + work.add(b); + break; + case Group: + work2 = execute(context, focus, exp.getGroup(), atEntry); + work.addAll(work2); + } + + if (exp.getInner() != null) + work = execute(context, work, exp.getInner(), false); + + if (exp.isProximal() && exp.getOperation() != null) { + ExpressionNode next = exp.getOpNext(); + ExpressionNode last = exp; + while (next != null) { + List work2 = preOperate(work, last.getOperation()); + if (work2 != null) + work = work2; + else if (last.getOperation() == Operation.Is || last.getOperation() == Operation.As) { + work2 = executeTypeName(context, focus, next, false); + work = operate(work, last.getOperation(), work2); + } else { + work2 = execute(context, focus, next, true); + work = operate(work, last.getOperation(), work2); +// System.out.println("Result of {'"+last.toString()+" "+last.getOperation().toCode()+" "+next.toString()+"'}: "+focus.toString()); + } + last = next; + next = next.getOpNext(); + } + } +// System.out.println("Result of {'"+exp.toString()+"'}: "+work.toString()); + return work; + } + + private List execute(ExecutionContext context, Base item, ExpressionNode exp, boolean atEntry) throws FHIRException { + List result = new ArrayList(); + if (atEntry && Character.isUpperCase(exp.getName().charAt(0))) {// special case for start up + if (item.isResource() && item.fhirType().equals(exp.getName())) + result.add(item); + } else + getChildrenByName(item, exp.getName(), result); + if (result.size() == 0 && atEntry && context.appInfo != null) { + // well, we didn't get a match on the name - we'll see if the name matches a constant known by the context. + // (if the name does match, and the user wants to get the constant value, they'll have to try harder... + Base temp = hostServices.resolveConstant(context.appInfo, exp.getName()); + if (temp != null) { + result.add(temp); } - result = organisePrecedence(lexer, result); } return result; } - private ExpressionNode organisePrecedence(FHIRLexer lexer, ExpressionNode node) { - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Times, Operation.DivideBy, Operation.Div, Operation.Mod)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Plus, Operation.Minus, Operation.Concatenate)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Union)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.LessThen, Operation.Greater, Operation.LessOrEqual, Operation.GreaterOrEqual)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Is)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Equals, Operation.Equivalent, Operation.NotEquals, Operation.NotEquivalent)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.And)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Xor, Operation.Or)); - // last: implies - return node; + private TypeDetails executeContextType(ExecutionTypeContext context, String name) throws PathEngineException, DefinitionException { + if (hostServices == null) + throw new PathEngineException("Unable to resolve context reference since no host services are provided"); + return hostServices.resolveConstantType(context.appInfo, name); + } + + private TypeDetails executeType(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { + TypeDetails result = new TypeDetails(null); + switch (exp.getKind()) { + case Name: + if (atEntry && exp.getName().equals("$this")) + result.update(context.getThisItem()); + else if (atEntry && exp.getName().equals("$total")) + result.update(anything(CollectionStatus.UNORDERED)); + else if (atEntry && focus == null) + result.update(executeContextType(context, exp.getName())); + else { + for (String s : focus.getTypes()) { + result.update(executeType(s, exp, atEntry)); + } + if (result.hasNoTypes()) + throw new PathEngineException("The name " + exp.getName() + " is not valid for any of the possible types: " + focus.describe()); + } + break; + case Function: + result.update(evaluateFunctionType(context, focus, exp)); + break; + case Constant: + result.update(resolveConstantType(context, exp.getConstant())); + break; + case Group: + result.update(executeType(context, focus, exp.getGroup(), atEntry)); + } + exp.setTypes(result); + + if (exp.getInner() != null) { + result = executeType(context, result, exp.getInner(), false); + } + + if (exp.isProximal() && exp.getOperation() != null) { + ExpressionNode next = exp.getOpNext(); + ExpressionNode last = exp; + while (next != null) { + TypeDetails work; + if (last.getOperation() == Operation.Is || last.getOperation() == Operation.As) + work = executeTypeName(context, focus, next, atEntry); + else + work = executeType(context, focus, next, atEntry); + result = operateTypes(result, last.getOperation(), work); + last = next; + next = next.getOpNext(); + } + exp.setOpTypes(result); + } + return result; + } + + private TypeDetails executeType(String type, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { + if (atEntry && Character.isUpperCase(exp.getName().charAt(0)) && hashTail(type).equals(exp.getName())) // special case for start up + return new TypeDetails(CollectionStatus.SINGLETON, type); + TypeDetails result = new TypeDetails(null); + getChildTypesByName(type, exp.getName(), result); + return result; + } + + private List executeTypeName(ExecutionContext context, List focus, ExpressionNode next, boolean atEntry) { + List result = new ArrayList(); + result.add(new StringType(next.getName())); + return result; + } + + private TypeDetails executeTypeName(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { + return new TypeDetails(CollectionStatus.SINGLETON, exp.getName()); + } + + private StructureDefinition fetchStructureByType(ElementDefinition ed) throws DefinitionException { + if (ed.getType().size() == 0) + throw new DefinitionException("Error in discriminator at " + ed.getId() + ": no children, no type"); + if (ed.getType().size() > 1) + throw new DefinitionException("Error in discriminator at " + ed.getId() + ": no children, multiple types"); + if (ed.getType().get(0).getProfile().size() > 1) + throw new DefinitionException("Error in discriminator at " + ed.getId() + ": no children, multiple type profiles"); + if (ed.hasSlicing()) + throw new DefinitionException("Error in discriminator at " + ed.getId() + ": slicing found"); + if (ed.getType().get(0).hasProfile()) + return worker.fetchResource(StructureDefinition.class, ed.getType().get(0).getProfile().get(0).getValue()); + else + return worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(ed.getType().get(0).getCode())); + } + + public String forLog() { + if (log.length() > 0) + return " (" + log.toString() + ")"; + else + return ""; + } + + private List funcAggregate(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List total = new ArrayList(); + if (exp.parameterCount() > 1) + total = execute(context, focus, exp.getParameters().get(1), false); + + List pc = new ArrayList(); + for (Base item : focus) { + ExecutionContext c = changeThis(context, item); + c.total = total; + total = execute(c, pc, exp.getParameters().get(0), true); + } + return total; + } + + private List funcAlias(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List nl = execute(context, focus, exp.getParameters().get(0), true); + String name = nl.get(0).primitiveValue(); + List res = new ArrayList(); + Base b = context.getAlias(name); + if (b != null) + res.add(b); + return res; + } + + private List funcAliasAs(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List nl = execute(context, focus, exp.getParameters().get(0), true); + String name = nl.get(0).primitiveValue(); + context.addAlias(name, focus); + return focus; + } + + private List funcAll(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + if (exp.getParameters().size() == 1) { + List result = new ArrayList(); + List pc = new ArrayList(); + boolean all = true; + for (Base item : focus) { + pc.clear(); + pc.add(item); + if (!convertToBoolean(execute(changeThis(context, item), pc, exp.getParameters().get(0), true))) { + all = false; + break; + } + } + result.add(new BooleanType(all).noExtensions()); + return result; + } else {// (exp.getParameters().size() == 0) { + List result = new ArrayList(); + boolean all = true; + for (Base item : focus) { + boolean v = false; + if (item instanceof BooleanType) { + v = ((BooleanType) item).booleanValue(); + } else + v = item != null; + if (!v) { + all = false; + break; + } + } + result.add(new BooleanType(all).noExtensions()); + return result; + } + } + + private List funcAs(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + String tn = exp.getParameters().get(0).getName(); + for (Base b : focus) + if (b.hasType(tn)) + result.add(b); + return result; + } + + private List funcChildren(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + for (Base b : focus) + getChildrenByName(b, "*", result); + return result; + } + + private List funcCombine(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + for (Base item : focus) { + result.add(item); + } + for (Base item : execute(context, focus, exp.getParameters().get(0), true)) { + result.add(item); + } + return result; + } + + private List funcContains(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) { + String st = convertToString(focus.get(0)); + if (Utilities.noString(st)) + result.add(new BooleanType(false).noExtensions()); + else + result.add(new BooleanType(st.contains(sw)).noExtensions()); + } else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcCount(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new IntegerType(focus.size()).noExtensions()); + return result; + } + + private List funcDescendants(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List current = new ArrayList(); + current.addAll(focus); + List added = new ArrayList(); + boolean more = true; + while (more) { + added.clear(); + for (Base item : current) { + getChildrenByName(item, "*", added); + } + more = !added.isEmpty(); + result.addAll(added); + current.clear(); + current.addAll(added); + } + return result; + } + + private List funcDistinct(ExecutionContext context, List focus, ExpressionNode exp) { + if (focus.size() <= 1) + return focus; + + List result = new ArrayList(); + for (int i = 0; i < focus.size(); i++) { + boolean found = false; + for (int j = i + 1; j < focus.size(); j++) { + if (doEquals(focus.get(j), focus.get(i))) { + found = true; + break; + } + } + if (!found) + result.add(focus.get(i)); + } + return result; + } + + private List funcEmpty(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new BooleanType(ElementUtil.isEmpty(focus)).noExtensions()); + return result; + } + + private List funcEndsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) + result.add(new BooleanType(convertToString(focus.get(0)).endsWith(sw)).noExtensions()); + else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcExclude(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List other = execute(context, focus, exp.getParameters().get(0), true); + + for (Base item : focus) { + if (!doContains(result, item) && !doContains(other, item)) + result.add(item); + } + return result; + } + + private List funcExists(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new BooleanType(!ElementUtil.isEmpty(focus)).noExtensions()); + return result; + } + + private List funcExtension(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List nl = execute(context, focus, exp.getParameters().get(0), true); + String url = nl.get(0).primitiveValue(); + + for (Base item : focus) { + List ext = new ArrayList(); + getChildrenByName(item, "extension", ext); + getChildrenByName(item, "modifierExtension", ext); + for (Base ex : ext) { + List vl = new ArrayList(); + getChildrenByName(ex, "url", vl); + if (convertToString(vl).equals(url)) + result.add(ex); + } + } + return result; + } + + private List funcFirst(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() > 0) + result.add(focus.get(0)); + return result; + } + + private List funcHasValue(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() == 1) { + String s = convertToString(focus.get(0)); + result.add(new BooleanType(!Utilities.noString(s)).noExtensions()); + } + return result; + } + + private List funcHtmlChecks(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + // todo: actually check the HTML + return makeBoolean(true); + } + + private List funcIif(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List n1 = execute(context, focus, exp.getParameters().get(0), true); + Boolean v = convertToBoolean(n1); + + if (v) + return execute(context, focus, exp.getParameters().get(1), true); + else if (exp.getParameters().size() < 3) + return new ArrayList(); + else + return execute(context, focus, exp.getParameters().get(2), true); + } + + private List funcIntersect(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List other = execute(context, focus, exp.getParameters().get(0), true); + + for (Base item : focus) { + if (!doContains(result, item) && doContains(other, item)) + result.add(item); + } + return result; + } + + private List funcIs(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { + if (focus.size() == 0 || focus.size() > 1) + return makeBoolean(false); + String ns = null; + String n = null; + + ExpressionNode texp = exp.getParameters().get(0); + if (texp.getKind() != Kind.Name) + throw new PathEngineException("Unsupported Expression type for Parameter on Is"); + if (texp.getInner() != null) { + if (texp.getInner().getKind() != Kind.Name) + throw new PathEngineException("Unsupported Expression type for Parameter on Is"); + ns = texp.getName(); + n = texp.getInner().getName(); + } else if (Utilities.existsInList(texp.getName(), "Boolean", "Integer", "Decimal", "String", "DateTime", "Time", "SimpleTypeInfo", "ClassInfo")) { + ns = "System"; + n = texp.getName(); + } else { + ns = "FHIR"; + n = texp.getName(); + } + if (ns.equals("System")) { + if (!(focus.get(0) instanceof Element) || ((Element) focus.get(0)).isDisallowExtensions()) + return makeBoolean(n.equals(Utilities.capitalize(focus.get(0).fhirType()))); + else + return makeBoolean(false); + } else if (ns.equals("FHIR")) { + return makeBoolean(n.equals(focus.get(0).fhirType())); + } else { + return makeBoolean(false); + } + } + + private List funcIsBoolean(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() != 1) + result.add(new BooleanType(false).noExtensions()); + else if (focus.get(0) instanceof IntegerType && ((IntegerType) focus.get(0)).getValue() >= 0) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof BooleanType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof StringType) + result.add(new BooleanType(Utilities.existsInList(convertToString(focus.get(0)), "true", "false")).noExtensions()); + else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcIsDateTime(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() != 1) + result.add(new BooleanType(false).noExtensions()); + else if (focus.get(0) instanceof DateTimeType || focus.get(0) instanceof DateType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof StringType) + result.add(new BooleanType((convertToString(focus.get(0)).matches + ("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?"))).noExtensions()); + else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcIsDecimal(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() != 1) + result.add(new BooleanType(false).noExtensions()); + else if (focus.get(0) instanceof IntegerType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof BooleanType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof DecimalType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof StringType) + result.add(new BooleanType(Utilities.isDecimal(convertToString(focus.get(0)))).noExtensions()); + else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcIsDistinct(ExecutionContext context, List focus, ExpressionNode exp) { + if (focus.size() <= 1) + return makeBoolean(true); + + boolean distinct = true; + for (int i = 0; i < focus.size(); i++) { + for (int j = i + 1; j < focus.size(); j++) { + if (doEquals(focus.get(j), focus.get(i))) { + distinct = false; + break; + } + } + } + return makeBoolean(distinct); + } + + private List funcIsInteger(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() != 1) + result.add(new BooleanType(false).noExtensions()); + else if (focus.get(0) instanceof IntegerType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof BooleanType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof StringType) + result.add(new BooleanType(Utilities.isInteger(convertToString(focus.get(0)))).noExtensions()); + else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcIsQuantity(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() != 1) + result.add(new BooleanType(false).noExtensions()); + else if (focus.get(0) instanceof IntegerType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof DecimalType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof Quantity) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof StringType) { + Quantity q = parseQuantityString(focus.get(0).primitiveValue()); + result.add(new BooleanType(q != null).noExtensions()); + } else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcIsString(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() != 1) + result.add(new BooleanType(false).noExtensions()); + else if (!(focus.get(0) instanceof DateTimeType) && !(focus.get(0) instanceof TimeType)) + result.add(new BooleanType(true).noExtensions()); + else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcIsTime(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() != 1) + result.add(new BooleanType(false).noExtensions()); + else if (focus.get(0) instanceof TimeType) + result.add(new BooleanType(true).noExtensions()); + else if (focus.get(0) instanceof StringType) + result.add(new BooleanType((convertToString(focus.get(0)).matches + ("T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?"))).noExtensions()); + else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcItem(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String s = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + if (Utilities.isInteger(s) && Integer.parseInt(s) < focus.size()) + result.add(focus.get(Integer.parseInt(s))); + return result; + } + + private List funcLast(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() > 0) + result.add(focus.get(focus.size() - 1)); + return result; + } + + private List funcLength(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() == 1) { + String s = convertToString(focus.get(0)); + result.add(new IntegerType(s.length()).noExtensions()); + } + return result; + } + + private List funcLower(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + if (focus.size() == 1) { + String s = convertToString(focus.get(0)); + if (!Utilities.noString(s)) + result.add(new StringType(s.toLowerCase()).noExtensions()); + } + return result; + } + + private List funcMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) { + String st = convertToString(focus.get(0)); + if (Utilities.noString(st)) + result.add(new BooleanType(false).noExtensions()); + else + result.add(new BooleanType(st.matches(sw)).noExtensions()); + } else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcMemberOf(ExecutionContext context, List focus, ExpressionNode exp) { + throw new Error("not Implemented yet"); + } + + private List funcNot(ExecutionContext context, List focus, ExpressionNode exp) { + return makeBoolean(!convertToBoolean(focus)); + } + + private List funcNow(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(DateTimeType.now()); + return result; + } + + private List funcRepeat(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List current = new ArrayList(); + current.addAll(focus); + List added = new ArrayList(); + boolean more = true; + while (more) { + added.clear(); + List pc = new ArrayList(); + for (Base item : current) { + pc.clear(); + pc.add(item); + added.addAll(execute(changeThis(context, item), pc, exp.getParameters().get(0), false)); + } + more = !added.isEmpty(); + result.addAll(added); + current.clear(); + current.addAll(added); + } + return result; + } + + private List funcReplace(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException, PathEngineException { + List result = new ArrayList(); + + if (focus.size() == 1) { + String f = convertToString(focus.get(0)); + + if (!Utilities.noString(f)) { + + if (exp.getParameters().size() != 2) { + + String t = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + String r = convertToString(execute(context, focus, exp.getParameters().get(1), true)); + + String n = f.replace(t, r); + result.add(new StringType(n)); + } else { + throw new PathEngineException(String.format("funcReplace() : checking for 2 arguments (pattern, substitution) but found %d items", exp.getParameters().size())); + } + } else { + throw new PathEngineException(String.format("funcReplace() : checking for 1 string item but found empty item")); + } + } else { + throw new PathEngineException(String.format("funcReplace() : checking for 1 string item but found %d items", focus.size())); + } + return result; + } + + private List funcReplaceMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) + result.add(new BooleanType(convertToString(focus.get(0)).contains(sw)).noExtensions()); + else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcResolve(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + for (Base item : focus) { + String s = convertToString(item); + if (item.fhirType().equals("Reference")) { + Property p = item.getChildByName("reference"); + if (p != null && p.hasValues()) + s = convertToString(p.getValues().get(0)); + else + s = null; // a reference without any valid actual reference (just identifier or display, but we can't resolve it) + } + if (item.fhirType().equals("canonical")) { + s = item.primitiveValue(); + } + if (s != null) { + Base res = null; + if (s.startsWith("#")) { + Property p = context.resource.getChildByName("contained"); + for (Base c : p.getValues()) { + if (s.equals(c.getIdBase())) { + res = c; + break; + } + } + } else if (hostServices != null) { + res = hostServices.resolveReference(context.appInfo, s); + } + if (res != null) + result.add(res); + } + } + + return result; + } + + private List funcSelect(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List pc = new ArrayList(); + for (Base item : focus) { + pc.clear(); + pc.add(item); + result.addAll(execute(changeThis(context, item), pc, exp.getParameters().get(0), true)); + } + return result; + } + + private List funcSingle(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { + if (focus.size() == 1) + return focus; + throw new PathEngineException(String.format("Single() : checking for 1 item but found %d items", focus.size())); + } + + private List funcSkip(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List n1 = execute(context, focus, exp.getParameters().get(0), true); + int i1 = Integer.parseInt(n1.get(0).primitiveValue()); + + List result = new ArrayList(); + for (int i = i1; i < focus.size(); i++) + result.add(focus.get(i)); + return result; + } + + private List funcStartsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + + if (focus.size() == 1 && !Utilities.noString(sw)) + result.add(new BooleanType(convertToString(focus.get(0)).startsWith(sw)).noExtensions()); + else + result.add(new BooleanType(false).noExtensions()); + return result; + } + + private List funcSubsetOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List target = execute(context, focus, exp.getParameters().get(0), true); + + boolean valid = true; + for (Base item : focus) { + boolean found = false; + for (Base t : target) { + if (Base.compareDeep(item, t, false)) { + found = true; + break; + } + } + if (!found) { + valid = false; + break; + } + } + List result = new ArrayList(); + result.add(new BooleanType(valid).noExtensions()); + return result; + } + + private List funcSubstring(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List n1 = execute(context, focus, exp.getParameters().get(0), true); + int i1 = Integer.parseInt(n1.get(0).primitiveValue()); + int i2 = -1; + if (exp.parameterCount() == 2) { + List n2 = execute(context, focus, exp.getParameters().get(1), true); + i2 = Integer.parseInt(n2.get(0).primitiveValue()); + } + + if (focus.size() == 1) { + String sw = convertToString(focus.get(0)); + String s; + if (i1 < 0 || i1 >= sw.length()) + return new ArrayList(); + if (exp.parameterCount() == 2) + s = sw.substring(i1, Math.min(sw.length(), i1 + i2)); + else + s = sw.substring(i1); + if (!Utilities.noString(s)) + result.add(new StringType(s).noExtensions()); + } + return result; + } + + private List funcSupersetOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List target = execute(context, focus, exp.getParameters().get(0), true); + + boolean valid = true; + for (Base item : target) { + boolean found = false; + for (Base t : focus) { + if (Base.compareDeep(item, t, false)) { + found = true; + break; + } + } + if (!found) { + valid = false; + break; + } + } + List result = new ArrayList(); + result.add(new BooleanType(valid).noExtensions()); + return result; + } + + private List funcTail(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + for (int i = 1; i < focus.size(); i++) + result.add(focus.get(i)); + return result; + } + + private List funcTake(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List n1 = execute(context, focus, exp.getParameters().get(0), true); + int i1 = Integer.parseInt(n1.get(0).primitiveValue()); + + List result = new ArrayList(); + for (int i = 0; i < Math.min(focus.size(), i1); i++) + result.add(focus.get(i)); + return result; + } + + private List funcToBoolean(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() == 1) { + if (focus.get(0) instanceof BooleanType) + result.add(focus.get(0)); + else if (focus.get(0) instanceof IntegerType) + result.add(new BooleanType(!focus.get(0).primitiveValue().equals("0")).noExtensions()); + else if (focus.get(0) instanceof StringType) { + if ("true".equals(focus.get(0).primitiveValue())) + result.add(new BooleanType(true).noExtensions()); + else if ("false".equals(focus.get(0).primitiveValue())) + result.add(new BooleanType(false).noExtensions()); + } + } + return result; + } + + private List funcToChars(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + if (focus.size() == 1) { + String s = convertToString(focus.get(0)); + for (char c : s.toCharArray()) + result.add(new StringType(String.valueOf(c)).noExtensions()); + } + return result; + } + + // private boolean isPrimitiveType(String s) { + // return s.equals("boolean") || s.equals("integer") || s.equals("decimal") || s.equals("base64Binary") || s.equals("instant") || s.equals("string") || s.equals("uri") || s.equals("date") || s.equals("dateTime") || s.equals("time") || s.equals("code") || s.equals("oid") || s.equals("id") || s.equals("unsignedInt") || s.equals("positiveInt") || s.equals("markdown"); + // } + + private List funcToDateTime(ExecutionContext context, List focus, ExpressionNode exp) { +// List result = new ArrayList(); +// result.add(new BooleanType(convertToBoolean(focus))); +// return result; + throw new NotImplementedException("funcToDateTime is not implemented"); + } + + private List funcToDecimal(ExecutionContext context, List focus, ExpressionNode exp) { + String s = convertToString(focus); + List result = new ArrayList(); + if (Utilities.isDecimal(s)) + result.add(new DecimalType(s).noExtensions()); + if ("true".equals(s)) + result.add(new DecimalType(1).noExtensions()); + if ("false".equals(s)) + result.add(new DecimalType(0).noExtensions()); + return result; + } + + private List funcToInteger(ExecutionContext context, List focus, ExpressionNode exp) { + String s = convertToString(focus); + List result = new ArrayList(); + if (Utilities.isInteger(s)) + result.add(new IntegerType(s).noExtensions()); + else if ("true".equals(s)) + result.add(new IntegerType(1).noExtensions()); + else if ("false".equals(s)) + result.add(new IntegerType(0).noExtensions()); + return result; + } + + private List funcToQuantity(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + if (focus.size() == 1) { + if (focus.get(0) instanceof Quantity) + result.add(focus.get(0)); + else if (focus.get(0) instanceof StringType) { + Quantity q = parseQuantityString(focus.get(0).primitiveValue()); + if (q != null) + result.add(q.noExtensions()); + } else if (focus.get(0) instanceof IntegerType) { + result.add(new Quantity().setValue(new BigDecimal(focus.get(0).primitiveValue())).setSystem("http://unitsofmeasure.org").setCode("1").noExtensions()); + } else if (focus.get(0) instanceof DecimalType) { + result.add(new Quantity().setValue(new BigDecimal(focus.get(0).primitiveValue())).setSystem("http://unitsofmeasure.org").setCode("1").noExtensions()); + } + } + return result; + } + + private List funcToString(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new StringType(convertToString(focus)).noExtensions()); + return result; + } + + private List funcToTime(ExecutionContext context, List focus, ExpressionNode exp) { +// List result = new ArrayList(); +// result.add(new BooleanType(convertToBoolean(focus))); +// return result; + throw new NotImplementedException("funcToTime is not implemented"); + } + + private List funcToday(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + result.add(new DateType(new Date(), TemporalPrecisionEnum.DAY)); + return result; + } + + private List funcTrace(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List nl = execute(context, focus, exp.getParameters().get(0), true); + String name = nl.get(0).primitiveValue(); + + log(name, focus); + return focus; + } + + private List funcType(ExecutionContext context, List focus, ExpressionNode exp) { + List result = new ArrayList(); + for (Base item : focus) + result.add(new ClassTypeInfo(item)); + return result; + } + + private List funcUnion(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + for (Base item : focus) { + if (!doContains(result, item)) + result.add(item); + } + for (Base item : execute(context, focus, exp.getParameters().get(0), true)) { + if (!doContains(result, item)) + result.add(item); + } + return result; + } + + private List funcUpper(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + if (focus.size() == 1) { + String s = convertToString(focus.get(0)); + if (!Utilities.noString(s)) + result.add(new StringType(s.toUpperCase()).noExtensions()); + } + return result; + } + + private List funcWhere(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + List result = new ArrayList(); + List pc = new ArrayList(); + for (Base item : focus) { + pc.clear(); + pc.add(item); + if (convertToBoolean(execute(changeThis(context, item), pc, exp.getParameters().get(0), true))) + result.add(item); + } + return result; } private ExpressionNode gatherPrecedence(FHIRLexer lexer, ExpressionNode start, EnumSet ops) { // work : boolean; // focus, node, group : ExpressionNode; - assert(start.isProximal()); + assert (start.isProximal()); // is there anything to do? boolean work = false; @@ -864,7 +2132,7 @@ public class FHIRPathEngine { work = work || ops.contains(focus.getOperation()); focus = focus.getOpNext(); } - } + } if (!work) return start; @@ -902,12 +2170,12 @@ public class FHIRPathEngine { // now look for another sequence, and start it ExpressionNode node = group; focus = group.getOpNext(); - if (focus != null) { + if (focus != null) { while (focus != null && !ops.contains(focus.getOperation())) { node = focus; focus = focus.getOpNext(); } - if (focus != null) { // && (focus.Operation in Ops) - must be true + if (focus != null) { // && (focus.Operation in Ops) - must be true group = newGroup(lexer, focus); node.setOpNext(group); } @@ -918,199 +2186,250 @@ public class FHIRPathEngine { return start; } + private void getChildTypesByName(String type, String name, TypeDetails result) throws PathEngineException, DefinitionException { + if (Utilities.noString(type)) + throw new PathEngineException("No type provided in BuildToolPathEvaluator.getChildTypesByName"); + if (type.equals("http://hl7.org/fhir/StructureDefinition/xhtml")) + return; + if (type.equals(TypeDetails.FP_SimpleTypeInfo)) { + getSimpleTypeChildTypesByName(name, result); + } else if (type.equals(TypeDetails.FP_ClassInfo)) { + getClassInfoChildTypesByName(name, result); + } else { + String url = null; + if (type.contains("#")) { + url = type.substring(0, type.indexOf("#")); + } else { + url = type; + } + String tail = ""; + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, url); + if (sd == null) + throw new DefinitionException("Unknown type " + type); // this really is an error, because we can only get to here if the internal infrastrucgture is wrong + List sdl = new ArrayList(); + ElementDefinitionMatch m = null; + if (type.contains("#")) + m = getElementDefinition(sd, type.substring(type.indexOf("#") + 1), false); + if (m != null && hasDataType(m.definition)) { + if (m.fixedType != null) { + StructureDefinition dt = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(m.fixedType)); + if (dt == null) + throw new DefinitionException("unknown data type " + m.fixedType); + sdl.add(dt); + } else + for (TypeRefComponent t : m.definition.getType()) { + StructureDefinition dt = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(t.getCode())); + if (dt == null) + throw new DefinitionException("unknown data type " + t.getCode()); + sdl.add(dt); + } + } else { + sdl.add(sd); + if (type.contains("#")) { + tail = type.substring(type.indexOf("#") + 1); + tail = tail.substring(tail.indexOf(".")); + } + } - private ExpressionNode newGroup(FHIRLexer lexer, ExpressionNode next) { - ExpressionNode result = new ExpressionNode(lexer.nextId()); - result.setKind(Kind.Group); - result.setGroup(next); - result.getGroup().setProximal(true); - return result; - } + for (StructureDefinition sdi : sdl) { + String path = sdi.getSnapshot().getElement().get(0).getPath() + tail + "."; + if (name.equals("**")) { + assert (result.getCollectionStatus() == CollectionStatus.UNORDERED); + for (ElementDefinition ed : sdi.getSnapshot().getElement()) { + if (ed.getPath().startsWith(path)) + for (TypeRefComponent t : ed.getType()) { + if (t.hasCode() && t.getCodeElement().hasValue()) { + String tn = null; + if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) + tn = sdi.getType() + "#" + ed.getPath(); + else + tn = t.getCode(); + if (t.getCode().equals("Resource")) { + for (String rn : worker.getResourceNames()) { + if (!result.hasType(worker, rn)) { + getChildTypesByName(result.addType(rn), "**", result); + } + } + } else if (!result.hasType(worker, tn)) { + getChildTypesByName(result.addType(tn), "**", result); + } + } + } + } + } else if (name.equals("*")) { + assert (result.getCollectionStatus() == CollectionStatus.UNORDERED); + for (ElementDefinition ed : sdi.getSnapshot().getElement()) { + if (ed.getPath().startsWith(path) && !ed.getPath().substring(path.length()).contains(".")) + for (TypeRefComponent t : ed.getType()) { + if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) + result.addType(sdi.getType() + "#" + ed.getPath()); + else if (t.getCode().equals("Resource")) + result.addTypes(worker.getResourceNames()); + else + result.addType(t.getCode()); + } + } + } else { + path = sdi.getSnapshot().getElement().get(0).getPath() + tail + "." + name; - private Base processConstant(FHIRLexer lexer) throws FHIRLexerException { - if (lexer.isStringConstant()) { - return new StringType(processConstantString(lexer.take(), lexer)).noExtensions(); - } else if (Utilities.isInteger(lexer.getCurrent())) { - return new IntegerType(lexer.take()).noExtensions(); - } else if (Utilities.isDecimal(lexer.getCurrent())) { - return new DecimalType(lexer.take()).noExtensions(); - } else if (Utilities.existsInList(lexer.getCurrent(), "true", "false")) { - return new BooleanType(lexer.take()).noExtensions(); - } else if (lexer.getCurrent().equals("{}")) { - lexer.take(); - return null; - } else if (lexer.getCurrent().startsWith("%") || lexer.getCurrent().startsWith("@")) { - return new FHIRConstant(lexer.take()); - } else - throw lexer.error("Invalid Constant "+lexer.getCurrent()); - } + ElementDefinitionMatch ed = getElementDefinition(sdi, path, false); + if (ed != null) { + if (!Utilities.noString(ed.getFixedType())) + result.addType(ed.getFixedType()); + else + for (TypeRefComponent t : ed.getDefinition().getType()) { + if (Utilities.noString(t.getCode())) + break; // throw new PathEngineException("Illegal reference to primitive value attribute @ "+path); - // procedure CheckParamCount(c : integer); - // begin - // if exp.Parameters.Count <> c then - // raise lexer.error('The function "'+exp.name+'" requires '+inttostr(c)+' parameters', offset); - // end; - - private boolean checkParamCount(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, int count) throws FHIRLexerException { - if (exp.getParameters().size() != count) - throw lexer.error("The function \""+exp.getName()+"\" requires "+Integer.toString(count)+" parameters", location.toString()); - return true; - } - - private boolean checkParamCount(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, int countMin, int countMax) throws FHIRLexerException { - if (exp.getParameters().size() < countMin || exp.getParameters().size() > countMax) - throw lexer.error("The function \""+exp.getName()+"\" requires between "+Integer.toString(countMin)+" and "+Integer.toString(countMax)+" parameters", location.toString()); - return true; - } - - private boolean checkParameters(FHIRLexer lexer, SourceLocation location, ExpressionNode exp, FunctionDetails details) throws FHIRLexerException { - switch (exp.getFunction()) { - case Empty: return checkParamCount(lexer, location, exp, 0); - case Not: return checkParamCount(lexer, location, exp, 0); - case Exists: return checkParamCount(lexer, location, exp, 0); - case SubsetOf: return checkParamCount(lexer, location, exp, 1); - case SupersetOf: return checkParamCount(lexer, location, exp, 1); - case IsDistinct: return checkParamCount(lexer, location, exp, 0); - case Distinct: return checkParamCount(lexer, location, exp, 0); - case Count: return checkParamCount(lexer, location, exp, 0); - case Where: return checkParamCount(lexer, location, exp, 1); - case Select: return checkParamCount(lexer, location, exp, 1); - case All: return checkParamCount(lexer, location, exp, 0, 1); - case Repeat: return checkParamCount(lexer, location, exp, 1); - case Aggregate: return checkParamCount(lexer, location, exp, 1, 2); - case Item: return checkParamCount(lexer, location, exp, 1); - case As: return checkParamCount(lexer, location, exp, 1); - case OfType: return checkParamCount(lexer, location, exp, 1); - case Type: return checkParamCount(lexer, location, exp, 0); - case Is: return checkParamCount(lexer, location, exp, 1); - case Single: return checkParamCount(lexer, location, exp, 0); - case First: return checkParamCount(lexer, location, exp, 0); - case Last: return checkParamCount(lexer, location, exp, 0); - case Tail: return checkParamCount(lexer, location, exp, 0); - case Skip: return checkParamCount(lexer, location, exp, 1); - case Take: return checkParamCount(lexer, location, exp, 1); - case Union: return checkParamCount(lexer, location, exp, 1); - case Combine: return checkParamCount(lexer, location, exp, 1); - case Intersect: return checkParamCount(lexer, location, exp, 1); - case Exclude: return checkParamCount(lexer, location, exp, 1); - case Iif: return checkParamCount(lexer, location, exp, 2,3); - case Lower: return checkParamCount(lexer, location, exp, 0); - case Upper: return checkParamCount(lexer, location, exp, 0); - case ToChars: return checkParamCount(lexer, location, exp, 0); - case Substring: return checkParamCount(lexer, location, exp, 1, 2); - case StartsWith: return checkParamCount(lexer, location, exp, 1); - case EndsWith: return checkParamCount(lexer, location, exp, 1); - case Matches: return checkParamCount(lexer, location, exp, 1); - case ReplaceMatches: return checkParamCount(lexer, location, exp, 2); - case Contains: return checkParamCount(lexer, location, exp, 1); - case Replace: return checkParamCount(lexer, location, exp, 2); - case Length: return checkParamCount(lexer, location, exp, 0); - case Children: return checkParamCount(lexer, location, exp, 0); - case Descendants: return checkParamCount(lexer, location, exp, 0); - case MemberOf: return checkParamCount(lexer, location, exp, 1); - case Trace: return checkParamCount(lexer, location, exp, 1); - case Today: return checkParamCount(lexer, location, exp, 0); - case Now: return checkParamCount(lexer, location, exp, 0); - case Resolve: return checkParamCount(lexer, location, exp, 0); - case Extension: return checkParamCount(lexer, location, exp, 1); - case HasValue: return checkParamCount(lexer, location, exp, 0); - case Alias: return checkParamCount(lexer, location, exp, 1); - case AliasAs: return checkParamCount(lexer, location, exp, 1); - case HtmlChecks: return checkParamCount(lexer, location, exp, 0); - case ToInteger: return checkParamCount(lexer, location, exp, 0); - case ToDecimal: return checkParamCount(lexer, location, exp, 0); - case ToString: return checkParamCount(lexer, location, exp, 0); - case ToQuantity: return checkParamCount(lexer, location, exp, 0); - case ToBoolean: return checkParamCount(lexer, location, exp, 0); - case ToDateTime: return checkParamCount(lexer, location, exp, 0); - case ToTime: return checkParamCount(lexer, location, exp, 0); - case IsInteger: return checkParamCount(lexer, location, exp, 0); - case IsDecimal: return checkParamCount(lexer, location, exp, 0); - case IsString: return checkParamCount(lexer, location, exp, 0); - case IsQuantity: return checkParamCount(lexer, location, exp, 0); - case IsBoolean: return checkParamCount(lexer, location, exp, 0); - case IsDateTime: return checkParamCount(lexer, location, exp, 0); - case IsTime: return checkParamCount(lexer, location, exp, 0); - case Custom: return checkParamCount(lexer, location, exp, details.getMinParameters(), details.getMaxParameters()); + ProfiledType pt = null; + if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) + pt = new ProfiledType(sdi.getUrl() + "#" + path); + else if (t.getCode().equals("Resource")) + result.addTypes(worker.getResourceNames()); + else + pt = new ProfiledType(t.getCode()); + if (pt != null) { + if (t.hasProfile()) + pt.addProfiles(t.getProfile()); + if (ed.getDefinition().hasBinding()) + pt.addBinding(ed.getDefinition().getBinding()); + result.addType(pt); + } + } + } + } + } } + } + + /** + * Given an item, return all the children that conform to the pattern described in name + *

    + * Possible patterns: + * - a simple name (which may be the base of a name with [] e.g. value[x]) + * - a name with a type replacement e.g. valueCodeableConcept + * - * which means all children + * - ** which means all descendants + * + * @param item + * @param name + * @param result + * @throws FHIRException + */ + protected void getChildrenByName(Base item, String name, List result) throws FHIRException { + Base[] list = item.listChildrenByName(name, false); + if (list != null) + for (Base v : list) + if (v != null) + result.add(v); + } + + private void getClassInfoChildTypesByName(String name, TypeDetails result) { + if (name.equals("namespace")) + result.addType(TypeDetails.FP_String); + if (name.equals("name")) + result.addType(TypeDetails.FP_String); + } + + private ElementDefinitionMatch getElementDefinition(StructureDefinition sd, String path, boolean allowTypedName) throws PathEngineException { + for (ElementDefinition ed : sd.getSnapshot().getElement()) { + if (ed.getPath().equals(path)) { + if (ed.hasContentReference()) { + return getElementDefinitionById(sd, ed.getContentReference()); + } else + return new ElementDefinitionMatch(ed, null); + } + if (ed.getPath().endsWith("[x]") && path.startsWith(ed.getPath().substring(0, ed.getPath().length() - 3)) && path.length() == ed.getPath().length() - 3) + return new ElementDefinitionMatch(ed, null); + if (allowTypedName && ed.getPath().endsWith("[x]") && path.startsWith(ed.getPath().substring(0, ed.getPath().length() - 3)) && path.length() > ed.getPath().length() - 3) { + String s = Utilities.uncapitalize(path.substring(ed.getPath().length() - 3)); + if (primitiveTypes.contains(s)) + return new ElementDefinitionMatch(ed, s); + else + return new ElementDefinitionMatch(ed, path.substring(ed.getPath().length() - 3)); + } + if (ed.getPath().contains(".") && path.startsWith(ed.getPath() + ".") && (ed.getType().size() > 0) && !isAbstractType(ed.getType())) { + // now we walk into the type. + if (ed.getType().size() > 1) // if there's more than one type, the test above would fail this + throw new PathEngineException("Internal typing issue...."); + StructureDefinition nsd = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(ed.getType().get(0).getCode())); + if (nsd == null) + throw new PathEngineException("Unknown type " + ed.getType().get(0).getCode()); + return getElementDefinition(nsd, nsd.getId() + path.substring(ed.getPath().length()), allowTypedName); + } + if (ed.hasContentReference() && path.startsWith(ed.getPath() + ".")) { + ElementDefinitionMatch m = getElementDefinitionById(sd, ed.getContentReference()); + return getElementDefinition(sd, m.definition.getPath() + path.substring(ed.getPath().length()), allowTypedName); + } + } + return null; + } + + private ElementDefinitionMatch getElementDefinitionById(StructureDefinition sd, String ref) { + for (ElementDefinition ed : sd.getSnapshot().getElement()) { + if (ref.equals("#" + ed.getId())) + return new ElementDefinitionMatch(ed, null); + } + return null; + } + + public IEvaluationContext getHostServices() { + return hostServices; + } + + public void setHostServices(IEvaluationContext constantResolver) { + this.hostServices = constantResolver; + } + + private void getSimpleTypeChildTypesByName(String name, TypeDetails result) { + if (name.equals("namespace")) + result.addType(TypeDetails.FP_String); + if (name.equals("name")) + result.addType(TypeDetails.FP_String); + } + + private boolean hasDataType(ElementDefinition ed) { + return ed.hasType() && !(ed.getType().get(0).getCode().equals("Element") || ed.getType().get(0).getCode().equals("BackboneElement")); + } + + public boolean hasLog() { + return log != null && log.length() > 0; + } + + private boolean hasType(ElementDefinition ed, String s) { + for (TypeRefComponent t : ed.getType()) + if (s.equalsIgnoreCase(t.getCode())) + return true; return false; } - private List execute(ExecutionContext context, List focus, ExpressionNode exp, boolean atEntry) throws FHIRException { -// System.out.println("Evaluate {'"+exp.toString()+"'} on "+focus.toString()); - List work = new ArrayList(); - switch (exp.getKind()) { - case Name: - if (atEntry && exp.getName().equals("$this")) - work.add(context.getThisItem()); - else if (atEntry && exp.getName().equals("$total")) - work.addAll(context.getTotal()); - else - for (Base item : focus) { - List outcome = execute(context, item, exp, atEntry); - for (Base base : outcome) - if (base != null) - work.add(base); - } - break; - case Function: - List work2 = evaluateFunction(context, focus, exp); - work.addAll(work2); - break; - case Constant: - Base b = resolveConstant(context, exp.getConstant()); - if (b != null) - work.add(b); - break; - case Group: - work2 = execute(context, focus, exp.getGroup(), atEntry); - work.addAll(work2); - } + private String hashTail(String type) { + return type.contains("#") ? "" : type.substring(type.lastIndexOf("/") + 1); + } - if (exp.getInner() != null) - work = execute(context, work, exp.getInner(), false); + private boolean isAbstractType(List list) { + return list.size() != 1 ? true : Utilities.existsInList(list.get(0).getCode(), "Element", "BackboneElement", "Resource", "DomainResource"); + } - if (exp.isProximal() && exp.getOperation() != null) { - ExpressionNode next = exp.getOpNext(); - ExpressionNode last = exp; - while (next != null) { - List work2 = preOperate(work, last.getOperation()); - if (work2 != null) - work = work2; - else if (last.getOperation() == Operation.Is || last.getOperation() == Operation.As) { - work2 = executeTypeName(context, focus, next, false); - work = operate(work, last.getOperation(), work2); - } else { - work2 = execute(context, focus, next, true); - work = operate(work, last.getOperation(), work2); -// System.out.println("Result of {'"+last.toString()+" "+last.getOperation().toCode()+" "+next.toString()+"'}: "+focus.toString()); - } - last = next; - next = next.getOpNext(); + private boolean isBoolean(List list, boolean b) { + return list.size() == 1 && list.get(0) instanceof BooleanType && ((BooleanType) list.get(0)).booleanValue() == b; + } + + private void log(String name, List contents) { + if (hostServices == null || !hostServices.log(name, contents)) { + if (log.length() > 0) + log.append("; "); + log.append(name); + log.append(": "); + boolean first = true; + for (Base b : contents) { + if (first) + first = false; + else + log.append(","); + log.append(convertToString(b)); } } -// System.out.println("Result of {'"+exp.toString()+"'}: "+work.toString()); - return work; - } - - private List executeTypeName(ExecutionContext context, List focus, ExpressionNode next, boolean atEntry) { - List result = new ArrayList(); - result.add(new StringType(next.getName())); - return result; - } - - - private List preOperate(List left, Operation operation) { - switch (operation) { - case And: - return isBoolean(left, false) ? makeBoolean(false) : null; - case Or: - return isBoolean(left, true) ? makeBoolean(true) : null; - case Implies: - return convertToBoolean(left) ? null : makeBoolean(true); - default: - return null; - } } private List makeBoolean(boolean b) { @@ -1119,201 +2438,25 @@ public class FHIRPathEngine { return res; } - private TypeDetails executeTypeName(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { - return new TypeDetails(CollectionStatus.SINGLETON, exp.getName()); - } - - private TypeDetails executeType(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { - TypeDetails result = new TypeDetails(null); - switch (exp.getKind()) { - case Name: - if (atEntry && exp.getName().equals("$this")) - result.update(context.getThisItem()); - else if (atEntry && exp.getName().equals("$total")) - result.update(anything(CollectionStatus.UNORDERED)); - else if (atEntry && focus == null) - result.update(executeContextType(context, exp.getName())); - else { - for (String s : focus.getTypes()) { - result.update(executeType(s, exp, atEntry)); - } - if (result.hasNoTypes()) - throw new PathEngineException("The name "+exp.getName()+" is not valid for any of the possible types: "+focus.describe()); - } - break; - case Function: - result.update(evaluateFunctionType(context, focus, exp)); - break; - case Constant: - result.update(resolveConstantType(context, exp.getConstant())); - break; - case Group: - result.update(executeType(context, focus, exp.getGroup(), atEntry)); - } - exp.setTypes(result); - - if (exp.getInner() != null) { - result = executeType(context, result, exp.getInner(), false); - } - - if (exp.isProximal() && exp.getOperation() != null) { - ExpressionNode next = exp.getOpNext(); - ExpressionNode last = exp; - while (next != null) { - TypeDetails work; - if (last.getOperation() == Operation.Is || last.getOperation() == Operation.As) - work = executeTypeName(context, focus, next, atEntry); - else - work = executeType(context, focus, next, atEntry); - result = operateTypes(result, last.getOperation(), work); - last = next; - next = next.getOpNext(); - } - exp.setOpTypes(result); - } + private ExpressionNode newGroup(FHIRLexer lexer, ExpressionNode next) { + ExpressionNode result = new ExpressionNode(lexer.nextId()); + result.setKind(Kind.Group); + result.setGroup(next); + result.getGroup().setProximal(true); return result; } - private Base resolveConstant(ExecutionContext context, Base constant) throws PathEngineException { - if (!(constant instanceof FHIRConstant)) - return constant; - FHIRConstant c = (FHIRConstant) constant; - if (c.getValue().startsWith("%")) { - return resolveConstant(context, c.getValue()); - } else if (c.getValue().startsWith("@")) { - return processDateConstant(context.appInfo, c.getValue().substring(1)); - } else - throw new PathEngineException("Invaild FHIR Constant "+c.getValue()); - } - - private Base processDateConstant(Object appInfo, String value) throws PathEngineException { - if (value.startsWith("T")) - return new TimeType(value.substring(1)).noExtensions(); - String v = value; - if (v.length() > 10) { - int i = v.substring(10).indexOf("-"); - if (i == -1) - i = v.substring(10).indexOf("+"); - if (i == -1) - i = v.substring(10).indexOf("Z"); - v = i == -1 ? value : v.substring(0, 10+i); - } - if (v.length() > 10) - return new DateTimeType(value).noExtensions(); - else - return new DateType(value).noExtensions(); - } - - - private Base resolveConstant(ExecutionContext context, String s) throws PathEngineException { - if (s.equals("%sct")) - return new StringType("http://snomed.info/sct").noExtensions(); - else if (s.equals("%loinc")) - return new StringType("http://loinc.org").noExtensions(); - else if (s.equals("%ucum")) - return new StringType("http://unitsofmeasure.org").noExtensions(); - else if (s.equals("%resource")) { - if (context.resource == null) - throw new PathEngineException("Cannot use %resource in this context"); - return context.resource; - } else if (s.equals("%context")) { - return context.context; - } else if (s.equals("%us-zip")) - return new StringType("[0-9]{5}(-[0-9]{4}){0,1}").noExtensions(); - else if (s.startsWith("%\"vs-")) - return new StringType("http://hl7.org/fhir/ValueSet/"+s.substring(5, s.length()-1)+"").noExtensions(); - else if (s.startsWith("%\"cs-")) - return new StringType("http://hl7.org/fhir/"+s.substring(5, s.length()-1)+"").noExtensions(); - else if (s.startsWith("%\"ext-")) - return new StringType("http://hl7.org/fhir/StructureDefinition/"+s.substring(6, s.length()-1)).noExtensions(); - else if (hostServices == null) - throw new PathEngineException("Unknown fixed constant '"+s+"'"); + private List opAnd(List left, List right) { + if (left.isEmpty() && right.isEmpty()) + return new ArrayList(); + else if (isBoolean(left, false) || isBoolean(right, false)) + return makeBoolean(false); + else if (left.isEmpty() || right.isEmpty()) + return new ArrayList(); + else if (convertToBoolean(left) && convertToBoolean(right)) + return makeBoolean(true); else - return hostServices.resolveConstant(context.appInfo, s.substring(1)); - } - - - private String processConstantString(String s, FHIRLexer lexer) throws FHIRLexerException { - StringBuilder b = new StringBuilder(); - int i = 1; - while (i < s.length()-1) { - char ch = s.charAt(i); - if (ch == '\\') { - i++; - switch (s.charAt(i)) { - case 't': - b.append('\t'); - break; - case 'r': - b.append('\r'); - break; - case 'n': - b.append('\n'); - break; - case 'f': - b.append('\f'); - break; - case '\'': - b.append('\''); - break; - case '"': - b.append('"'); - break; - case '\\': - b.append('\\'); - break; - case '/': - b.append('/'); - break; - case 'u': - i++; - int uc = Integer.parseInt(s.substring(i, i+4), 16); - b.append((char) uc); - i = i + 3; - break; - default: - throw lexer.error("Unknown character escape \\"+s.charAt(i)); - } - i++; - } else { - b.append(ch); - i++; - } - } - return b.toString(); - } - - - private List operate(List left, Operation operation, List right) throws FHIRException { - switch (operation) { - case Equals: return opEquals(left, right); - case Equivalent: return opEquivalent(left, right); - case NotEquals: return opNotEquals(left, right); - case NotEquivalent: return opNotEquivalent(left, right); - case LessThen: return opLessThen(left, right); - case Greater: return opGreater(left, right); - case LessOrEqual: return opLessOrEqual(left, right); - case GreaterOrEqual: return opGreaterOrEqual(left, right); - case Union: return opUnion(left, right); - case In: return opIn(left, right); - case MemberOf: return opMemberOf(left, right); - case Contains: return opContains(left, right); - case Or: return opOr(left, right); - case And: return opAnd(left, right); - case Xor: return opXor(left, right); - case Implies: return opImplies(left, right); - case Plus: return opPlus(left, right); - case Times: return opTimes(left, right); - case Minus: return opMinus(left, right); - case Concatenate: return opConcatenate(left, right); - case DivideBy: return opDivideBy(left, right); - case Div: return opDiv(left, right); - case Mod: return opMod(left, right); - case Is: return opIs(left, right); - case As: return opAs(left, right); - default: - throw new Error("Not Done Yet: "+operation.toCode()); - } + return makeBoolean(false); } private List opAs(List left, List right) { @@ -1328,405 +2471,12 @@ public class FHIRPathEngine { return result; } - - private List opIs(List left, List right) { + private List opConcatenate(List left, List right) { List result = new ArrayList(); - if (left.size() != 1 || right.size() != 1) - result.add(new BooleanType(false).noExtensions()); - else { - String tn = convertToString(right); - if (left.get(0) instanceof org.hl7.fhir.r4.elementmodel.Element) - result.add(new BooleanType(left.get(0).hasType(tn)).noExtensions()); - else if ((left.get(0) instanceof Element) || ((Element) left.get(0)).isDisallowExtensions()) - result.add(new BooleanType(Utilities.capitalize(left.get(0).fhirType()).equals(tn)).noExtensions()); - else - result.add(new BooleanType(left.get(0).hasType(tn)).noExtensions()); - } + result.add(new StringType(convertToString(left) + convertToString((right)))); return result; } - - private TypeDetails operateTypes(TypeDetails left, Operation operation, TypeDetails right) { - switch (operation) { - case Equals: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Equivalent: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case NotEquals: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case NotEquivalent: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case LessThen: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Greater: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case LessOrEqual: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case GreaterOrEqual: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Is: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case As: return new TypeDetails(CollectionStatus.SINGLETON, right.getTypes()); - case Union: return left.union(right); - case Or: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case And: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Xor: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Implies : return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Times: - TypeDetails result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType(TypeDetails.FP_Integer); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType(TypeDetails.FP_Decimal); - return result; - case DivideBy: - result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType(TypeDetails.FP_Decimal); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType(TypeDetails.FP_Decimal); - return result; - case Concatenate: - result = new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); - return result; - case Plus: - result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType(TypeDetails.FP_Integer); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType(TypeDetails.FP_Decimal); - else if (left.hasType(worker, "string", "id", "code", "uri") && right.hasType(worker, "string", "id", "code", "uri")) - result.addType(TypeDetails.FP_String); - return result; - case Minus: - result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType(TypeDetails.FP_Integer); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType(TypeDetails.FP_Decimal); - return result; - case Div: - case Mod: - result = new TypeDetails(CollectionStatus.SINGLETON); - if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) - result.addType(TypeDetails.FP_Integer); - else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) - result.addType(TypeDetails.FP_Decimal); - return result; - case In: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case MemberOf: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Contains: return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - default: - return null; - } - } - - - private List opEquals(List left, List right) { - if (left.size() != right.size()) - return makeBoolean(false); - - boolean res = true; - for (int i = 0; i < left.size(); i++) { - if (!doEquals(left.get(i), right.get(i))) { - res = false; - break; - } - } - return makeBoolean(res); - } - - private List opNotEquals(List left, List right) { - if (left.size() != right.size()) - return makeBoolean(true); - - boolean res = true; - for (int i = 0; i < left.size(); i++) { - if (!doEquals(left.get(i), right.get(i))) { - res = false; - break; - } - } - return makeBoolean(!res); - } - - private boolean doEquals(Base left, Base right) { - if (left instanceof Quantity && right instanceof Quantity) - return qtyEqual((Quantity) left, (Quantity) right); - else if (left.isPrimitive() && right.isPrimitive()) - return Base.equals(left.primitiveValue(), right.primitiveValue()); - else - return Base.compareDeep(left, right, false); - } - - - private boolean doEquivalent(Base left, Base right) throws PathEngineException { - if (left instanceof Quantity && right instanceof Quantity) - return qtyEquivalent((Quantity) left, (Quantity) right); - if (left.hasType("integer") && right.hasType("integer")) - return doEquals(left, right); - if (left.hasType("boolean") && right.hasType("boolean")) - return doEquals(left, right); - if (left.hasType("integer", "decimal", "unsignedInt", "positiveInt") && right.hasType("integer", "decimal", "unsignedInt", "positiveInt")) - return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); - if (left.hasType("date", "dateTime", "time", "instant") && right.hasType("date", "dateTime", "time", "instant")) - return compareDateTimeElements(left, right, true) == 0; - if (left.hasType("string", "id", "code", "uri") && right.hasType("string", "id", "code", "uri")) - return Utilities.equivalent(convertToString(left), convertToString(right)); - - throw new PathEngineException(String.format("Unable to determine equivalence between %s and %s", left.fhirType(), right.fhirType())); - } - - private boolean qtyEqual(Quantity left, Quantity right) { - if (worker.getUcumService() != null) { - DecimalType dl = qtyToCanonical(left); - DecimalType dr = qtyToCanonical(right); - if (dl != null && dr != null) - return doEquals(dl, dr); - } - return left.equals(right); - } - - private DecimalType qtyToCanonical(Quantity q) { - if (!"http://unitsofmeasure.org".equals(q.getSystem())) - return null; - try { - Pair p = new Pair(new Decimal(q.getValue().toPlainString()), q.getCode()); - Pair c = worker.getUcumService().getCanonicalForm(p); - return new DecimalType(c.getValue().asDecimal()); - } catch (UcumException e) { - return null; - } - } - - private Base pairToQty(Pair p) { - return new Quantity().setValue(new BigDecimal(p.getValue().toString())).setSystem("http://unitsofmeasure.org").setCode(p.getCode()).noExtensions(); - } - - - private Pair qtyToPair(Quantity q) { - if (!"http://unitsofmeasure.org".equals(q.getSystem())) - return null; - try { - return new Pair(new Decimal(q.getValue().toPlainString()), q.getCode()); - } catch (UcumException e) { - return null; - } - } - - - private boolean qtyEquivalent(Quantity left, Quantity right) throws PathEngineException { - if (worker.getUcumService() != null) { - DecimalType dl = qtyToCanonical(left); - DecimalType dr = qtyToCanonical(right); - if (dl != null && dr != null) - return doEquivalent(dl, dr); - } - return left.equals(right); - } - - - - private List opEquivalent(List left, List right) throws PathEngineException { - if (left.size() != right.size()) - return makeBoolean(false); - - boolean res = true; - for (int i = 0; i < left.size(); i++) { - boolean found = false; - for (int j = 0; j < right.size(); j++) { - if (doEquivalent(left.get(i), right.get(j))) { - found = true; - break; - } - } - if (!found) { - res = false; - break; - } - } - return makeBoolean(res); - } - - private List opNotEquivalent(List left, List right) throws PathEngineException { - if (left.size() != right.size()) - return makeBoolean(true); - - boolean res = true; - for (int i = 0; i < left.size(); i++) { - boolean found = false; - for (int j = 0; j < right.size(); j++) { - if (doEquivalent(left.get(i), right.get(j))) { - found = true; - break; - } - } - if (!found) { - res = false; - break; - } - } - return makeBoolean(!res); - } - - private List opLessThen(List left, List right) throws FHIRException { - if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { - Base l = left.get(0); - Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); - else if ((l.hasType("integer") || l.hasType("decimal")) && (r.hasType("integer") || r.hasType("decimal"))) - return makeBoolean(new Double(l.primitiveValue()) < new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(compareDateTimeElements(l, r, false) < 0); - else if ((l.hasType("time")) && (r.hasType("time"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); - } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { - List lUnit = left.get(0).listChildrenByName("code"); - List rUnit = right.get(0).listChildrenByName("code"); - if (Base.compareDeep(lUnit, rUnit, true)) { - return opLessThen(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); - } else { - if (worker.getUcumService() == null) - return makeBoolean(false); - else { - List dl = new ArrayList(); - dl.add(qtyToCanonical((Quantity) left.get(0))); - List dr = new ArrayList(); - dr.add(qtyToCanonical((Quantity) right.get(0))); - return opLessThen(dl, dr); - } - } - } - return new ArrayList(); - } - - private List opGreater(List left, List right) throws FHIRException { - if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { - Base l = left.get(0); - Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); - else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) - return makeBoolean(new Double(l.primitiveValue()) > new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(compareDateTimeElements(l, r, false) > 0); - else if ((l.hasType("time")) && (r.hasType("time"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); - } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { - List lUnit = left.get(0).listChildrenByName("unit"); - List rUnit = right.get(0).listChildrenByName("unit"); - if (Base.compareDeep(lUnit, rUnit, true)) { - return opGreater(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); - } else { - if (worker.getUcumService() == null) - return makeBoolean(false); - else { - List dl = new ArrayList(); - dl.add(qtyToCanonical((Quantity) left.get(0))); - List dr = new ArrayList(); - dr.add(qtyToCanonical((Quantity) right.get(0))); - return opGreater(dl, dr); - } - } - } - return new ArrayList(); - } - - private List opLessOrEqual(List left, List right) throws FHIRException { - if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { - Base l = left.get(0); - Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); - else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) - return makeBoolean(new Double(l.primitiveValue()) <= new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(compareDateTimeElements(l, r, false) <= 0); - else if ((l.hasType("time")) && (r.hasType("time"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); - } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { - List lUnits = left.get(0).listChildrenByName("unit"); - String lunit = lUnits.size() == 1 ? lUnits.get(0).primitiveValue() : null; - List rUnits = right.get(0).listChildrenByName("unit"); - String runit = rUnits.size() == 1 ? rUnits.get(0).primitiveValue() : null; - if ((lunit == null && runit == null) || lunit.equals(runit)) { - return opLessOrEqual(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); - } else { - if (worker.getUcumService() == null) - return makeBoolean(false); - else { - List dl = new ArrayList(); - dl.add(qtyToCanonical((Quantity) left.get(0))); - List dr = new ArrayList(); - dr.add(qtyToCanonical((Quantity) right.get(0))); - return opLessOrEqual(dl, dr); - } - } - } - return new ArrayList(); - } - - private List opGreaterOrEqual(List left, List right) throws FHIRException { - if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { - Base l = left.get(0); - Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); - else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) - return makeBoolean(new Double(l.primitiveValue()) >= new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(compareDateTimeElements(l, r, false) >= 0); - else if ((l.hasType("time")) && (r.hasType("time"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); - } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { - List lUnit = left.get(0).listChildrenByName("unit"); - List rUnit = right.get(0).listChildrenByName("unit"); - if (Base.compareDeep(lUnit, rUnit, true)) { - return opGreaterOrEqual(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); - } else { - if (worker.getUcumService() == null) - return makeBoolean(false); - else { - List dl = new ArrayList(); - dl.add(qtyToCanonical((Quantity) left.get(0))); - List dr = new ArrayList(); - dr.add(qtyToCanonical((Quantity) right.get(0))); - return opGreaterOrEqual(dl, dr); - } - } - } - return new ArrayList(); - } - - private List opMemberOf(List left, List right) throws FHIRException { - boolean ans = false; - ValueSet vs = worker.fetchResource(ValueSet.class, right.get(0).primitiveValue()); - if (vs != null) { - for (Base l : left) { - if (l.fhirType().equals("code")) { - if (worker.validateCode(l.castToCoding(l), vs).isOk()) - ans = true; - } else if (l.fhirType().equals("Coding")) { - if (worker.validateCode(l.castToCoding(l), vs).isOk()) - ans = true; - } else if (l.fhirType().equals("CodeableConcept")) { - if (worker.validateCode(l.castToCodeableConcept(l), vs).isOk()) - ans = true; - } - } - } - return makeBoolean(ans); - } - - private List opIn(List left, List right) throws FHIRException { - boolean ans = true; - for (Base l : left) { - boolean f = false; - for (Base r : right) - if (doEquals(l, r)) { - f = true; - break; - } - if (!f) { - ans = false; - break; - } - } - return makeBoolean(ans); - } - private List opContains(List left, List right) { boolean ans = true; for (Base r : right) { @@ -1744,168 +2494,37 @@ public class FHIRPathEngine { return makeBoolean(ans); } - private List opPlus(List left, List right) throws PathEngineException { + private List opDiv(List left, List right) throws PathEngineException { if (left.size() == 0) - throw new PathEngineException("Error performing +: left operand has no value"); + throw new PathEngineException("Error performing div: left operand has no value"); if (left.size() > 1) - throw new PathEngineException("Error performing +: left operand has more than one value"); - if (!left.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing +: left operand has the wrong type (%s)", left.get(0).fhirType())); - if (right.size() == 0) - throw new PathEngineException("Error performing +: right operand has no value"); - if (right.size() > 1) - throw new PathEngineException("Error performing +: right operand has more than one value"); - if (!right.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing +: right operand has the wrong type (%s)", right.get(0).fhirType())); - - List result = new ArrayList(); - Base l = left.get(0); - Base r = right.get(0); - if (l.hasType("string", "id", "code", "uri") && r.hasType("string", "id", "code", "uri")) - result.add(new StringType(l.primitiveValue() + r.primitiveValue())); - else if (l.hasType("integer") && r.hasType("integer")) - result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) + Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) - result.add(new DecimalType(new BigDecimal(l.primitiveValue()).add(new BigDecimal(r.primitiveValue())))); - else - throw new PathEngineException(String.format("Error performing +: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); - return result; - } - - private List opTimes(List left, List right) throws PathEngineException { - if (left.size() == 0) - throw new PathEngineException("Error performing *: left operand has no value"); - if (left.size() > 1) - throw new PathEngineException("Error performing *: left operand has more than one value"); + throw new PathEngineException("Error performing div: left operand has more than one value"); if (!left.get(0).isPrimitive() && !(left.get(0) instanceof Quantity)) - throw new PathEngineException(String.format("Error performing +: left operand has the wrong type (%s)", left.get(0).fhirType())); + throw new PathEngineException(String.format("Error performing div: left operand has the wrong type (%s)", left.get(0).fhirType())); if (right.size() == 0) - throw new PathEngineException("Error performing *: right operand has no value"); + throw new PathEngineException("Error performing div: right operand has no value"); if (right.size() > 1) - throw new PathEngineException("Error performing *: right operand has more than one value"); + throw new PathEngineException("Error performing div: right operand has more than one value"); if (!right.get(0).isPrimitive() && !(right.get(0) instanceof Quantity)) - throw new PathEngineException(String.format("Error performing *: right operand has the wrong type (%s)", right.get(0).fhirType())); + throw new PathEngineException(String.format("Error performing div: right operand has the wrong type (%s)", right.get(0).fhirType())); List result = new ArrayList(); Base l = left.get(0); Base r = right.get(0); - if (l.hasType("integer") && r.hasType("integer")) - result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) * Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) - result.add(new DecimalType(new BigDecimal(l.primitiveValue()).multiply(new BigDecimal(r.primitiveValue())))); - else if (l instanceof Quantity && r instanceof Quantity && worker.getUcumService() != null) { - Pair pl = qtyToPair((Quantity) l); - Pair pr = qtyToPair((Quantity) r); - Pair p; + if (l.hasType("integer") && r.hasType("integer")) + result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) / Integer.parseInt(r.primitiveValue()))); + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { + Decimal d1; try { - p = worker.getUcumService().multiply(pl, pr); - result.add(pairToQty(p)); + d1 = new Decimal(l.primitiveValue()); + Decimal d2 = new Decimal(r.primitiveValue()); + result.add(new IntegerType(d1.divInt(d2).asDecimal())); } catch (UcumException e) { - throw new PathEngineException(e.getMessage(), e); + throw new PathEngineException(e); } } else - throw new PathEngineException(String.format("Error performing *: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); - return result; - } - - - private List opConcatenate(List left, List right) { - List result = new ArrayList(); - result.add(new StringType(convertToString(left) + convertToString((right)))); - return result; - } - - private List opUnion(List left, List right) { - List result = new ArrayList(); - for (Base item : left) { - if (!doContains(result, item)) - result.add(item); - } - for (Base item : right) { - if (!doContains(result, item)) - result.add(item); - } - return result; - } - - private boolean doContains(List list, Base item) { - for (Base test : list) - if (doEquals(test, item)) - return true; - return false; - } - - - private List opAnd(List left, List right) { - if (left.isEmpty() && right.isEmpty()) - return new ArrayList(); - else if (isBoolean(left, false) || isBoolean(right, false)) - return makeBoolean(false); - else if (left.isEmpty() || right.isEmpty()) - return new ArrayList(); - else if (convertToBoolean(left) && convertToBoolean(right)) - return makeBoolean(true); - else - return makeBoolean(false); - } - - private boolean isBoolean(List list, boolean b) { - return list.size() == 1 && list.get(0) instanceof BooleanType && ((BooleanType) list.get(0)).booleanValue() == b; - } - - private List opOr(List left, List right) { - if (left.isEmpty() && right.isEmpty()) - return new ArrayList(); - else if (convertToBoolean(left) || convertToBoolean(right)) - return makeBoolean(true); - else if (left.isEmpty() || right.isEmpty()) - return new ArrayList(); - else - return makeBoolean(false); - } - - private List opXor(List left, List right) { - if (left.isEmpty() || right.isEmpty()) - return new ArrayList(); - else - return makeBoolean(convertToBoolean(left) ^ convertToBoolean(right)); - } - - private List opImplies(List left, List right) { - if (!convertToBoolean(left)) - return makeBoolean(true); - else if (right.size() == 0) - return new ArrayList(); - else - return makeBoolean(convertToBoolean(right)); - } - - - private List opMinus(List left, List right) throws PathEngineException { - if (left.size() == 0) - throw new PathEngineException("Error performing -: left operand has no value"); - if (left.size() > 1) - throw new PathEngineException("Error performing -: left operand has more than one value"); - if (!left.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing -: left operand has the wrong type (%s)", left.get(0).fhirType())); - if (right.size() == 0) - throw new PathEngineException("Error performing -: right operand has no value"); - if (right.size() > 1) - throw new PathEngineException("Error performing -: right operand has more than one value"); - if (!right.get(0).isPrimitive()) - throw new PathEngineException(String.format("Error performing -: right operand has the wrong type (%s)", right.get(0).fhirType())); - - List result = new ArrayList(); - Base l = left.get(0); - Base r = right.get(0); - - if (l.hasType("integer") && r.hasType("integer")) - result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) - Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) - result.add(new DecimalType(new BigDecimal(l.primitiveValue()).subtract(new BigDecimal(r.primitiveValue())))); - else - throw new PathEngineException(String.format("Error performing -: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); + throw new PathEngineException(String.format("Error performing div: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); return result; } @@ -1951,38 +2570,257 @@ public class FHIRPathEngine { return result; } - private List opDiv(List left, List right) throws PathEngineException { + private List opEquals(List left, List right) { + if (left.size() != right.size()) + return makeBoolean(false); + + boolean res = true; + for (int i = 0; i < left.size(); i++) { + if (!doEquals(left.get(i), right.get(i))) { + res = false; + break; + } + } + return makeBoolean(res); + } + + private List opEquivalent(List left, List right) throws PathEngineException { + if (left.size() != right.size()) + return makeBoolean(false); + + boolean res = true; + for (int i = 0; i < left.size(); i++) { + boolean found = false; + for (int j = 0; j < right.size(); j++) { + if (doEquivalent(left.get(i), right.get(j))) { + found = true; + break; + } + } + if (!found) { + res = false; + break; + } + } + return makeBoolean(res); + } + + private List opGreater(List left, List right) throws FHIRException { + if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { + Base l = left.get(0); + Base r = right.get(0); + if (l.hasType("string") && r.hasType("string")) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); + else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) + return makeBoolean(new Double(l.primitiveValue()) > new Double(r.primitiveValue())); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r, false) > 0); + else if ((l.hasType("time")) && (r.hasType("time"))) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); + } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity")) { + List lUnit = left.get(0).listChildrenByName("unit"); + List rUnit = right.get(0).listChildrenByName("unit"); + if (Base.compareDeep(lUnit, rUnit, true)) { + return opGreater(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); + } else { + if (worker.getUcumService() == null) + return makeBoolean(false); + else { + List dl = new ArrayList(); + dl.add(qtyToCanonical((Quantity) left.get(0))); + List dr = new ArrayList(); + dr.add(qtyToCanonical((Quantity) right.get(0))); + return opGreater(dl, dr); + } + } + } + return new ArrayList(); + } + + private List opGreaterOrEqual(List left, List right) throws FHIRException { + if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { + Base l = left.get(0); + Base r = right.get(0); + if (l.hasType("string") && r.hasType("string")) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); + else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) + return makeBoolean(new Double(l.primitiveValue()) >= new Double(r.primitiveValue())); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r, false) >= 0); + else if ((l.hasType("time")) && (r.hasType("time"))) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); + } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity")) { + List lUnit = left.get(0).listChildrenByName("unit"); + List rUnit = right.get(0).listChildrenByName("unit"); + if (Base.compareDeep(lUnit, rUnit, true)) { + return opGreaterOrEqual(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); + } else { + if (worker.getUcumService() == null) + return makeBoolean(false); + else { + List dl = new ArrayList(); + dl.add(qtyToCanonical((Quantity) left.get(0))); + List dr = new ArrayList(); + dr.add(qtyToCanonical((Quantity) right.get(0))); + return opGreaterOrEqual(dl, dr); + } + } + } + return new ArrayList(); + } + + private List opImplies(List left, List right) { + if (!convertToBoolean(left)) + return makeBoolean(true); + else if (right.size() == 0) + return new ArrayList(); + else + return makeBoolean(convertToBoolean(right)); + } + + private List opIn(List left, List right) throws FHIRException { + boolean ans = true; + for (Base l : left) { + boolean f = false; + for (Base r : right) + if (doEquals(l, r)) { + f = true; + break; + } + if (!f) { + ans = false; + break; + } + } + return makeBoolean(ans); + } + + private List opIs(List left, List right) { + List result = new ArrayList(); + if (left.size() != 1 || right.size() != 1) + result.add(new BooleanType(false).noExtensions()); + else { + String tn = convertToString(right); + if (left.get(0) instanceof org.hl7.fhir.r4.elementmodel.Element) + result.add(new BooleanType(left.get(0).hasType(tn)).noExtensions()); + else if ((left.get(0) instanceof Element) || ((Element) left.get(0)).isDisallowExtensions()) + result.add(new BooleanType(Utilities.capitalize(left.get(0).fhirType()).equals(tn)).noExtensions()); + else + result.add(new BooleanType(left.get(0).hasType(tn)).noExtensions()); + } + return result; + } + + private List opLessOrEqual(List left, List right) throws FHIRException { + if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { + Base l = left.get(0); + Base r = right.get(0); + if (l.hasType("string") && r.hasType("string")) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); + else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) + return makeBoolean(new Double(l.primitiveValue()) <= new Double(r.primitiveValue())); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r, false) <= 0); + else if ((l.hasType("time")) && (r.hasType("time"))) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); + } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity")) { + List lUnits = left.get(0).listChildrenByName("unit"); + String lunit = lUnits.size() == 1 ? lUnits.get(0).primitiveValue() : null; + List rUnits = right.get(0).listChildrenByName("unit"); + String runit = rUnits.size() == 1 ? rUnits.get(0).primitiveValue() : null; + if ((lunit == null && runit == null) || lunit.equals(runit)) { + return opLessOrEqual(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); + } else { + if (worker.getUcumService() == null) + return makeBoolean(false); + else { + List dl = new ArrayList(); + dl.add(qtyToCanonical((Quantity) left.get(0))); + List dr = new ArrayList(); + dr.add(qtyToCanonical((Quantity) right.get(0))); + return opLessOrEqual(dl, dr); + } + } + } + return new ArrayList(); + } + + private List opLessThen(List left, List right) throws FHIRException { + if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { + Base l = left.get(0); + Base r = right.get(0); + if (l.hasType("string") && r.hasType("string")) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); + else if ((l.hasType("integer") || l.hasType("decimal")) && (r.hasType("integer") || r.hasType("decimal"))) + return makeBoolean(new Double(l.primitiveValue()) < new Double(r.primitiveValue())); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r, false) < 0); + else if ((l.hasType("time")) && (r.hasType("time"))) + return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); + } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity")) { + List lUnit = left.get(0).listChildrenByName("code"); + List rUnit = right.get(0).listChildrenByName("code"); + if (Base.compareDeep(lUnit, rUnit, true)) { + return opLessThen(left.get(0).listChildrenByName("value"), right.get(0).listChildrenByName("value")); + } else { + if (worker.getUcumService() == null) + return makeBoolean(false); + else { + List dl = new ArrayList(); + dl.add(qtyToCanonical((Quantity) left.get(0))); + List dr = new ArrayList(); + dr.add(qtyToCanonical((Quantity) right.get(0))); + return opLessThen(dl, dr); + } + } + } + return new ArrayList(); + } + + private List opMemberOf(List left, List right) throws FHIRException { + boolean ans = false; + ValueSet vs = worker.fetchResource(ValueSet.class, right.get(0).primitiveValue()); + if (vs != null) { + for (Base l : left) { + if (l.fhirType().equals("code")) { + if (worker.validateCode(l.castToCoding(l), vs).isOk()) + ans = true; + } else if (l.fhirType().equals("Coding")) { + if (worker.validateCode(l.castToCoding(l), vs).isOk()) + ans = true; + } else if (l.fhirType().equals("CodeableConcept")) { + if (worker.validateCode(l.castToCodeableConcept(l), vs).isOk()) + ans = true; + } + } + } + return makeBoolean(ans); + } + + private List opMinus(List left, List right) throws PathEngineException { if (left.size() == 0) - throw new PathEngineException("Error performing div: left operand has no value"); + throw new PathEngineException("Error performing -: left operand has no value"); if (left.size() > 1) - throw new PathEngineException("Error performing div: left operand has more than one value"); - if (!left.get(0).isPrimitive() && !(left.get(0) instanceof Quantity)) - throw new PathEngineException(String.format("Error performing div: left operand has the wrong type (%s)", left.get(0).fhirType())); + throw new PathEngineException("Error performing -: left operand has more than one value"); + if (!left.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing -: left operand has the wrong type (%s)", left.get(0).fhirType())); if (right.size() == 0) - throw new PathEngineException("Error performing div: right operand has no value"); + throw new PathEngineException("Error performing -: right operand has no value"); if (right.size() > 1) - throw new PathEngineException("Error performing div: right operand has more than one value"); - if (!right.get(0).isPrimitive() && !(right.get(0) instanceof Quantity)) - throw new PathEngineException(String.format("Error performing div: right operand has the wrong type (%s)", right.get(0).fhirType())); + throw new PathEngineException("Error performing -: right operand has more than one value"); + if (!right.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing -: right operand has the wrong type (%s)", right.get(0).fhirType())); List result = new ArrayList(); Base l = left.get(0); Base r = right.get(0); - if (l.hasType("integer") && r.hasType("integer")) - result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) / Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { - Decimal d1; - try { - d1 = new Decimal(l.primitiveValue()); - Decimal d2 = new Decimal(r.primitiveValue()); - result.add(new IntegerType(d1.divInt(d2).asDecimal())); - } catch (UcumException e) { - throw new PathEngineException(e); - } - } + if (l.hasType("integer") && r.hasType("integer")) + result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) - Integer.parseInt(r.primitiveValue()))); + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) + result.add(new DecimalType(new BigDecimal(l.primitiveValue()).subtract(new BigDecimal(r.primitiveValue())))); else - throw new PathEngineException(String.format("Error performing div: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); + throw new PathEngineException(String.format("Error performing -: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); return result; } @@ -2004,7 +2842,7 @@ public class FHIRPathEngine { Base l = left.get(0); Base r = right.get(0); - if (l.hasType("integer") && r.hasType("integer")) + if (l.hasType("integer") && r.hasType("integer")) result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) % Integer.parseInt(r.primitiveValue()))); else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { Decimal d1; @@ -2015,15 +2853,670 @@ public class FHIRPathEngine { } catch (UcumException e) { throw new PathEngineException(e); } - } - else + } else throw new PathEngineException(String.format("Error performing mod: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); return result; } + private List opNotEquals(List left, List right) { + if (left.size() != right.size()) + return makeBoolean(true); + + boolean res = true; + for (int i = 0; i < left.size(); i++) { + if (!doEquals(left.get(i), right.get(i))) { + res = false; + break; + } + } + return makeBoolean(!res); + } + + private List opNotEquivalent(List left, List right) throws PathEngineException { + if (left.size() != right.size()) + return makeBoolean(true); + + boolean res = true; + for (int i = 0; i < left.size(); i++) { + boolean found = false; + for (int j = 0; j < right.size(); j++) { + if (doEquivalent(left.get(i), right.get(j))) { + found = true; + break; + } + } + if (!found) { + res = false; + break; + } + } + return makeBoolean(!res); + } + + private List opOr(List left, List right) { + if (left.isEmpty() && right.isEmpty()) + return new ArrayList(); + else if (convertToBoolean(left) || convertToBoolean(right)) + return makeBoolean(true); + else if (left.isEmpty() || right.isEmpty()) + return new ArrayList(); + else + return makeBoolean(false); + } + + private List opPlus(List left, List right) throws PathEngineException { + if (left.size() == 0) + throw new PathEngineException("Error performing +: left operand has no value"); + if (left.size() > 1) + throw new PathEngineException("Error performing +: left operand has more than one value"); + if (!left.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing +: left operand has the wrong type (%s)", left.get(0).fhirType())); + if (right.size() == 0) + throw new PathEngineException("Error performing +: right operand has no value"); + if (right.size() > 1) + throw new PathEngineException("Error performing +: right operand has more than one value"); + if (!right.get(0).isPrimitive()) + throw new PathEngineException(String.format("Error performing +: right operand has the wrong type (%s)", right.get(0).fhirType())); + + List result = new ArrayList(); + Base l = left.get(0); + Base r = right.get(0); + if (l.hasType("string", "id", "code", "uri") && r.hasType("string", "id", "code", "uri")) + result.add(new StringType(l.primitiveValue() + r.primitiveValue())); + else if (l.hasType("integer") && r.hasType("integer")) + result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) + Integer.parseInt(r.primitiveValue()))); + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) + result.add(new DecimalType(new BigDecimal(l.primitiveValue()).add(new BigDecimal(r.primitiveValue())))); + else + throw new PathEngineException(String.format("Error performing +: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); + return result; + } + + private List opTimes(List left, List right) throws PathEngineException { + if (left.size() == 0) + throw new PathEngineException("Error performing *: left operand has no value"); + if (left.size() > 1) + throw new PathEngineException("Error performing *: left operand has more than one value"); + if (!left.get(0).isPrimitive() && !(left.get(0) instanceof Quantity)) + throw new PathEngineException(String.format("Error performing +: left operand has the wrong type (%s)", left.get(0).fhirType())); + if (right.size() == 0) + throw new PathEngineException("Error performing *: right operand has no value"); + if (right.size() > 1) + throw new PathEngineException("Error performing *: right operand has more than one value"); + if (!right.get(0).isPrimitive() && !(right.get(0) instanceof Quantity)) + throw new PathEngineException(String.format("Error performing *: right operand has the wrong type (%s)", right.get(0).fhirType())); + + List result = new ArrayList(); + Base l = left.get(0); + Base r = right.get(0); + + if (l.hasType("integer") && r.hasType("integer")) + result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) * Integer.parseInt(r.primitiveValue()))); + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) + result.add(new DecimalType(new BigDecimal(l.primitiveValue()).multiply(new BigDecimal(r.primitiveValue())))); + else if (l instanceof Quantity && r instanceof Quantity && worker.getUcumService() != null) { + Pair pl = qtyToPair((Quantity) l); + Pair pr = qtyToPair((Quantity) r); + Pair p; + try { + p = worker.getUcumService().multiply(pl, pr); + result.add(pairToQty(p)); + } catch (UcumException e) { + throw new PathEngineException(e.getMessage(), e); + } + } else + throw new PathEngineException(String.format("Error performing *: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); + return result; + } + + private List opUnion(List left, List right) { + List result = new ArrayList(); + for (Base item : left) { + if (!doContains(result, item)) + result.add(item); + } + for (Base item : right) { + if (!doContains(result, item)) + result.add(item); + } + return result; + } + + private List opXor(List left, List right) { + if (left.isEmpty() || right.isEmpty()) + return new ArrayList(); + else + return makeBoolean(convertToBoolean(left) ^ convertToBoolean(right)); + } + + private List operate(List left, Operation operation, List right) throws FHIRException { + switch (operation) { + case Equals: + return opEquals(left, right); + case Equivalent: + return opEquivalent(left, right); + case NotEquals: + return opNotEquals(left, right); + case NotEquivalent: + return opNotEquivalent(left, right); + case LessThen: + return opLessThen(left, right); + case Greater: + return opGreater(left, right); + case LessOrEqual: + return opLessOrEqual(left, right); + case GreaterOrEqual: + return opGreaterOrEqual(left, right); + case Union: + return opUnion(left, right); + case In: + return opIn(left, right); + case MemberOf: + return opMemberOf(left, right); + case Contains: + return opContains(left, right); + case Or: + return opOr(left, right); + case And: + return opAnd(left, right); + case Xor: + return opXor(left, right); + case Implies: + return opImplies(left, right); + case Plus: + return opPlus(left, right); + case Times: + return opTimes(left, right); + case Minus: + return opMinus(left, right); + case Concatenate: + return opConcatenate(left, right); + case DivideBy: + return opDivideBy(left, right); + case Div: + return opDiv(left, right); + case Mod: + return opMod(left, right); + case Is: + return opIs(left, right); + case As: + return opAs(left, right); + default: + throw new Error("Not Done Yet: " + operation.toCode()); + } + } + + private TypeDetails operateTypes(TypeDetails left, Operation operation, TypeDetails right) { + switch (operation) { + case Equals: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Equivalent: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case NotEquals: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case NotEquivalent: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case LessThen: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Greater: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case LessOrEqual: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case GreaterOrEqual: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Is: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case As: + return new TypeDetails(CollectionStatus.SINGLETON, right.getTypes()); + case Union: + return left.union(right); + case Or: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case And: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Xor: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Implies: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Times: + TypeDetails result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType(TypeDetails.FP_Integer); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType(TypeDetails.FP_Decimal); + return result; + case DivideBy: + result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType(TypeDetails.FP_Decimal); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType(TypeDetails.FP_Decimal); + return result; + case Concatenate: + result = new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); + return result; + case Plus: + result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType(TypeDetails.FP_Integer); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType(TypeDetails.FP_Decimal); + else if (left.hasType(worker, "string", "id", "code", "uri") && right.hasType(worker, "string", "id", "code", "uri")) + result.addType(TypeDetails.FP_String); + return result; + case Minus: + result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType(TypeDetails.FP_Integer); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType(TypeDetails.FP_Decimal); + return result; + case Div: + case Mod: + result = new TypeDetails(CollectionStatus.SINGLETON); + if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) + result.addType(TypeDetails.FP_Integer); + else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) + result.addType(TypeDetails.FP_Decimal); + return result; + case In: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case MemberOf: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + case Contains: + return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); + default: + return null; + } + } + + private ExpressionNode organisePrecedence(FHIRLexer lexer, ExpressionNode node) { + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Times, Operation.DivideBy, Operation.Div, Operation.Mod)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Plus, Operation.Minus, Operation.Concatenate)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Union)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.LessThen, Operation.Greater, Operation.LessOrEqual, Operation.GreaterOrEqual)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Is)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Equals, Operation.Equivalent, Operation.NotEquals, Operation.NotEquivalent)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.And)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Xor, Operation.Or)); + // last: implies + return node; + } + + private Base pairToQty(Pair p) { + return new Quantity().setValue(new BigDecimal(p.getValue().toString())).setSystem("http://unitsofmeasure.org").setCode(p.getCode()).noExtensions(); + } + + /** + * Parse a path for later use using execute + * + * @param path + * @return + * @throws PathEngineException + * @throws Exception + */ + public ExpressionNode parse(String path) throws FHIRLexerException { + FHIRLexer lexer = new FHIRLexer(path); + if (lexer.done()) + throw lexer.error("Path cannot be empty"); + ExpressionNode result = parseExpression(lexer, true); + if (!lexer.done()) + throw lexer.error("Premature ExpressionNode termination at unexpected token \"" + lexer.getCurrent() + "\""); + result.check(); + return result; + } + + /** + * Parse a path that is part of some other syntax + * + * @return + * @throws PathEngineException + * @throws Exception + */ + public ExpressionNode parse(FHIRLexer lexer) throws FHIRLexerException { + ExpressionNode result = parseExpression(lexer, true); + result.check(); + return result; + } + + private ExpressionNode parseExpression(FHIRLexer lexer, boolean proximal) throws FHIRLexerException { + ExpressionNode result = new ExpressionNode(lexer.nextId()); + SourceLocation c = lexer.getCurrentStartLocation(); + result.setStart(lexer.getCurrentLocation()); + // special: + if (lexer.getCurrent().equals("-")) { + lexer.take(); + lexer.setCurrent("-" + lexer.getCurrent()); + } + if (lexer.getCurrent().equals("+")) { + lexer.take(); + lexer.setCurrent("+" + lexer.getCurrent()); + } + if (lexer.isConstant(false)) { + boolean isString = lexer.isStringConstant(); + result.setConstant(processConstant(lexer)); + result.setKind(Kind.Constant); + if (!isString && !lexer.done() && (result.getConstant() instanceof IntegerType || result.getConstant() instanceof DecimalType) && (lexer.isStringConstant() || lexer.hasToken("year", "years", "month", "months", "week", "weeks", "day", "days", "hour", "hours", "minute", "minutes", "second", "seconds", "millisecond", "milliseconds"))) { + // it's a quantity + String ucum = null; + if (lexer.hasToken("year", "years", "month", "months", "week", "weeks", "day", "days", "hour", "hours", "minute", "minutes", "second", "seconds", "millisecond", "milliseconds")) { + String s = lexer.take(); + if (s.equals("year") || s.equals("years")) + ucum = "a"; + else if (s.equals("month") || s.equals("months")) + ucum = "mo"; + else if (s.equals("week") || s.equals("weeks")) + ucum = "wk"; + else if (s.equals("day") || s.equals("days")) + ucum = "d"; + else if (s.equals("hour") || s.equals("hours")) + ucum = "h"; + else if (s.equals("minute") || s.equals("minutes")) + ucum = "min"; + else if (s.equals("second") || s.equals("seconds")) + ucum = "s"; + else // (s.equals("millisecond") || s.equals("milliseconds")) + ucum = "ms"; + } else + ucum = lexer.readConstant("units"); + result.setConstant(new Quantity().setValue(new BigDecimal(result.getConstant().primitiveValue())).setSystem("http://unitsofmeasure.org").setCode(ucum)); + } + result.setEnd(lexer.getCurrentLocation()); + } else if ("(".equals(lexer.getCurrent())) { + lexer.next(); + result.setKind(Kind.Group); + result.setGroup(parseExpression(lexer, true)); + if (!")".equals(lexer.getCurrent())) + throw lexer.error("Found " + lexer.getCurrent() + " expecting a \")\""); + result.setEnd(lexer.getCurrentLocation()); + lexer.next(); + } else { + if (!lexer.isToken() && !lexer.getCurrent().startsWith("\"")) + throw lexer.error("Found " + lexer.getCurrent() + " expecting a token name"); + if (lexer.getCurrent().startsWith("\"")) + result.setName(lexer.readConstant("Path Name")); + else + result.setName(lexer.take()); + result.setEnd(lexer.getCurrentLocation()); + if (!result.checkName()) + throw lexer.error("Found " + result.getName() + " expecting a valid token name"); + if ("(".equals(lexer.getCurrent())) { + Function f = Function.fromCode(result.getName()); + FunctionDetails details = null; + if (f == null) { + if (hostServices != null) + details = hostServices.resolveFunction(result.getName()); + if (details == null) + throw lexer.error("The name " + result.getName() + " is not a valid function name"); + f = Function.Custom; + } + result.setKind(Kind.Function); + result.setFunction(f); + lexer.next(); + while (!")".equals(lexer.getCurrent())) { + result.getParameters().add(parseExpression(lexer, true)); + if (",".equals(lexer.getCurrent())) + lexer.next(); + else if (!")".equals(lexer.getCurrent())) + throw lexer.error("The token " + lexer.getCurrent() + " is not expected here - either a \",\" or a \")\" expected"); + } + result.setEnd(lexer.getCurrentLocation()); + lexer.next(); + checkParameters(lexer, c, result, details); + } else + result.setKind(Kind.Name); + } + ExpressionNode focus = result; + if ("[".equals(lexer.getCurrent())) { + lexer.next(); + ExpressionNode item = new ExpressionNode(lexer.nextId()); + item.setKind(Kind.Function); + item.setFunction(ExpressionNode.Function.Item); + item.getParameters().add(parseExpression(lexer, true)); + if (!lexer.getCurrent().equals("]")) + throw lexer.error("The token " + lexer.getCurrent() + " is not expected here - a \"]\" expected"); + lexer.next(); + result.setInner(item); + focus = item; + } + if (".".equals(lexer.getCurrent())) { + lexer.next(); + focus.setInner(parseExpression(lexer, false)); + } + result.setProximal(proximal); + if (proximal) { + while (lexer.isOp()) { + focus.setOperation(ExpressionNode.Operation.fromCode(lexer.getCurrent())); + focus.setOpStart(lexer.getCurrentStartLocation()); + focus.setOpEnd(lexer.getCurrentLocation()); + lexer.next(); + focus.setOpNext(parseExpression(lexer, false)); + focus = focus.getOpNext(); + } + result = organisePrecedence(lexer, result); + } + return result; + } + + public Quantity parseQuantityString(String s) { + if (s == null) + return null; + s = s.trim(); + if (s.contains(" ")) { + String v = s.substring(0, s.indexOf(" ")).trim(); + s = s.substring(s.indexOf(" ")).trim(); + if (!Utilities.isDecimal(v)) + return null; + if (s.startsWith("'") && s.endsWith("'")) + return Quantity.fromUcum(v, s.substring(1, s.length() - 1)); + if (s.equals("year") || s.equals("years")) + return Quantity.fromUcum(v, "a"); + else if (s.equals("month") || s.equals("months")) + return Quantity.fromUcum(v, "mo"); + else if (s.equals("week") || s.equals("weeks")) + return Quantity.fromUcum(v, "wk"); + else if (s.equals("day") || s.equals("days")) + return Quantity.fromUcum(v, "d"); + else if (s.equals("hour") || s.equals("hours")) + return Quantity.fromUcum(v, "h"); + else if (s.equals("minute") || s.equals("minutes")) + return Quantity.fromUcum(v, "min"); + else if (s.equals("second") || s.equals("seconds")) + return Quantity.fromUcum(v, "s"); + else if (s.equals("millisecond") || s.equals("milliseconds")) + return Quantity.fromUcum(v, "ms"); + else + return null; + } else { + if (Utilities.isDecimal(s)) + return new Quantity().setValue(new BigDecimal(s)).setSystem("http://unitsofmeasure.org").setCode("1"); + else + return null; + } + } + + private List preOperate(List left, Operation operation) { + switch (operation) { + case And: + return isBoolean(left, false) ? makeBoolean(false) : null; + case Or: + return isBoolean(left, true) ? makeBoolean(true) : null; + case Implies: + return convertToBoolean(left) ? null : makeBoolean(true); + default: + return null; + } + } + + private Base processConstant(FHIRLexer lexer) throws FHIRLexerException { + if (lexer.isStringConstant()) { + return new StringType(processConstantString(lexer.take(), lexer)).noExtensions(); + } else if (Utilities.isInteger(lexer.getCurrent())) { + return new IntegerType(lexer.take()).noExtensions(); + } else if (Utilities.isDecimal(lexer.getCurrent())) { + return new DecimalType(lexer.take()).noExtensions(); + } else if (Utilities.existsInList(lexer.getCurrent(), "true", "false")) { + return new BooleanType(lexer.take()).noExtensions(); + } else if (lexer.getCurrent().equals("{}")) { + lexer.take(); + return null; + } else if (lexer.getCurrent().startsWith("%") || lexer.getCurrent().startsWith("@")) { + return new FHIRConstant(lexer.take()); + } else + throw lexer.error("Invalid Constant " + lexer.getCurrent()); + } + + private String processConstantString(String s, FHIRLexer lexer) throws FHIRLexerException { + StringBuilder b = new StringBuilder(); + int i = 1; + while (i < s.length() - 1) { + char ch = s.charAt(i); + if (ch == '\\') { + i++; + switch (s.charAt(i)) { + case 't': + b.append('\t'); + break; + case 'r': + b.append('\r'); + break; + case 'n': + b.append('\n'); + break; + case 'f': + b.append('\f'); + break; + case '\'': + b.append('\''); + break; + case '"': + b.append('"'); + break; + case '\\': + b.append('\\'); + break; + case '/': + b.append('/'); + break; + case 'u': + i++; + int uc = Integer.parseInt(s.substring(i, i + 4), 16); + b.append((char) uc); + i = i + 3; + break; + default: + throw lexer.error("Unknown character escape \\" + s.charAt(i)); + } + i++; + } else { + b.append(ch); + i++; + } + } + return b.toString(); + } + + private Base processDateConstant(Object appInfo, String value) throws PathEngineException { + if (value.startsWith("T")) + return new TimeType(value.substring(1)).noExtensions(); + String v = value; + if (v.length() > 10) { + int i = v.substring(10).indexOf("-"); + if (i == -1) + i = v.substring(10).indexOf("+"); + if (i == -1) + i = v.substring(10).indexOf("Z"); + v = i == -1 ? value : v.substring(0, 10 + i); + } + if (v.length() > 10) + return new DateTimeType(value).noExtensions(); + else + return new DateType(value).noExtensions(); + } + + private boolean qtyEqual(Quantity left, Quantity right) { + if (worker.getUcumService() != null) { + DecimalType dl = qtyToCanonical(left); + DecimalType dr = qtyToCanonical(right); + if (dl != null && dr != null) + return doEquals(dl, dr); + } + return left.equals(right); + } + + private boolean qtyEquivalent(Quantity left, Quantity right) throws PathEngineException { + if (worker.getUcumService() != null) { + DecimalType dl = qtyToCanonical(left); + DecimalType dr = qtyToCanonical(right); + if (dl != null && dr != null) + return doEquivalent(dl, dr); + } + return left.equals(right); + } + + private DecimalType qtyToCanonical(Quantity q) { + if (!"http://unitsofmeasure.org".equals(q.getSystem())) + return null; + try { + Pair p = new Pair(new Decimal(q.getValue().toPlainString()), q.getCode()); + Pair c = worker.getUcumService().getCanonicalForm(p); + return new DecimalType(c.getValue().asDecimal()); + } catch (UcumException e) { + return null; + } + } + + private Pair qtyToPair(Quantity q) { + if (!"http://unitsofmeasure.org".equals(q.getSystem())) + return null; + try { + return new Pair(new Decimal(q.getValue().toPlainString()), q.getCode()); + } catch (UcumException e) { + return null; + } + } + + private Base resolveConstant(ExecutionContext context, Base constant) throws PathEngineException { + if (!(constant instanceof FHIRConstant)) + return constant; + FHIRConstant c = (FHIRConstant) constant; + if (c.getValue().startsWith("%")) { + return resolveConstant(context, c.getValue()); + } else if (c.getValue().startsWith("@")) { + return processDateConstant(context.appInfo, c.getValue().substring(1)); + } else + throw new PathEngineException("Invaild FHIR Constant " + c.getValue()); + } + + private Base resolveConstant(ExecutionContext context, String s) throws PathEngineException { + if (s.equals("%sct")) + return new StringType("http://snomed.info/sct").noExtensions(); + else if (s.equals("%loinc")) + return new StringType("http://loinc.org").noExtensions(); + else if (s.equals("%ucum")) + return new StringType("http://unitsofmeasure.org").noExtensions(); + else if (s.equals("%resource")) { + if (context.resource == null) + throw new PathEngineException("Cannot use %resource in this context"); + return context.resource; + } else if (s.equals("%context")) { + return context.context; + } else if (s.equals("%us-zip")) + return new StringType("[0-9]{5}(-[0-9]{4}){0,1}").noExtensions(); + else if (s.startsWith("%\"vs-")) + return new StringType("http://hl7.org/fhir/ValueSet/" + s.substring(5, s.length() - 1) + "").noExtensions(); + else if (s.startsWith("%\"cs-")) + return new StringType("http://hl7.org/fhir/" + s.substring(5, s.length() - 1) + "").noExtensions(); + else if (s.startsWith("%\"ext-")) + return new StringType("http://hl7.org/fhir/StructureDefinition/" + s.substring(6, s.length() - 1)).noExtensions(); + else if (hostServices == null) + throw new PathEngineException("Unknown fixed constant '" + s + "'"); + else + return hostServices.resolveConstant(context.appInfo, s.substring(1)); + } private TypeDetails resolveConstantType(ExecutionTypeContext context, Base constant) throws PathEngineException { - if (constant instanceof BooleanType) + if (constant instanceof BooleanType) return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); else if (constant instanceof IntegerType) return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); @@ -2066,1563 +3559,27 @@ public class FHIRPathEngine { else if (s.startsWith("%\"ext-")) return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); else if (hostServices == null) - throw new PathEngineException("Unknown fixed constant type for '"+s+"'"); + throw new PathEngineException("Unknown fixed constant type for '" + s + "'"); else return hostServices.resolveConstantType(context.appInfo, s); } - private List execute(ExecutionContext context, Base item, ExpressionNode exp, boolean atEntry) throws FHIRException { - List result = new ArrayList(); - if (atEntry && Character.isUpperCase(exp.getName().charAt(0))) {// special case for start up - if (item.isResource() && item.fhirType().equals(exp.getName())) - result.add(item); - } else - getChildrenByName(item, exp.getName(), result); - if (result.size() == 0 && atEntry && context.appInfo != null) { - // well, we didn't get a match on the name - we'll see if the name matches a constant known by the context. - // (if the name does match, and the user wants to get the constant value, they'll have to try harder... - Base temp = hostServices.resolveConstant(context.appInfo, exp.getName()); - if (temp != null) { - result.add(temp); - } - } - return result; - } - - private TypeDetails executeContextType(ExecutionTypeContext context, String name) throws PathEngineException, DefinitionException { - if (hostServices == null) - throw new PathEngineException("Unable to resolve context reference since no host services are provided"); - return hostServices.resolveConstantType(context.appInfo, name); - } - - private TypeDetails executeType(String type, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { - if (atEntry && Character.isUpperCase(exp.getName().charAt(0)) && hashTail(type).equals(exp.getName())) // special case for start up - return new TypeDetails(CollectionStatus.SINGLETON, type); - TypeDetails result = new TypeDetails(null); - getChildTypesByName(type, exp.getName(), result); - return result; - } - - - private String hashTail(String type) { - return type.contains("#") ? "" : type.substring(type.lastIndexOf("/")+1); - } - - - @SuppressWarnings("unchecked") - private TypeDetails evaluateFunctionType(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp) throws PathEngineException, DefinitionException { - List paramTypes = new ArrayList(); - if (exp.getFunction() == Function.Is || exp.getFunction() == Function.As) - paramTypes.add(new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - else - for (ExpressionNode expr : exp.getParameters()) { - if (exp.getFunction() == Function.Where || exp.getFunction() == Function.Select || exp.getFunction() == Function.Repeat || exp.getFunction() == Function.Aggregate) - paramTypes.add(executeType(changeThis(context, focus), focus, expr, true)); - else - paramTypes.add(executeType(context, focus, expr, true)); - } - switch (exp.getFunction()) { - case Empty : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Not : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Exists : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case SubsetOf : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case SupersetOf : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case IsDistinct : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Distinct : - return focus; - case Count : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); - case Where : - return focus; - case Select : - return anything(focus.getCollectionStatus()); - case All : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Repeat : - return anything(focus.getCollectionStatus()); - case Aggregate : - return anything(focus.getCollectionStatus()); - case Item : { - checkOrdered(focus, "item"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); - return focus; - } - case As : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, exp.getParameters().get(0).getName()); - } - case OfType : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, exp.getParameters().get(0).getName()); - } - case Type : { - boolean s = false; - boolean c = false; - for (ProfiledType pt : focus.getProfiledTypes()) { - s = s || pt.isSystemType(); - c = c || !pt.isSystemType(); - } - if (s && c) - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_SimpleTypeInfo, TypeDetails.FP_ClassInfo); - else if (s) - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_SimpleTypeInfo); - else - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_ClassInfo); - } - case Is : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case Single : - return focus.toSingleton(); - case First : { - checkOrdered(focus, "first"); - return focus.toSingleton(); - } - case Last : { - checkOrdered(focus, "last"); - return focus.toSingleton(); - } - case Tail : { - checkOrdered(focus, "tail"); - return focus; - } - case Skip : { - checkOrdered(focus, "skip"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); - return focus; - } - case Take : { - checkOrdered(focus, "take"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); - return focus; - } - case Union : { - return focus.union(paramTypes.get(0)); - } - case Combine : { - return focus.union(paramTypes.get(0)); - } - case Intersect : { - return focus.intersect(paramTypes.get(0)); - } - case Exclude : { - return focus; - } - case Iif : { - TypeDetails types = new TypeDetails(null); - types.update(paramTypes.get(0)); - if (paramTypes.size() > 1) - types.update(paramTypes.get(1)); - return types; - } - case Lower : { - checkContextString(focus, "lower"); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); - } - case Upper : { - checkContextString(focus, "upper"); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); - } - case ToChars : { - checkContextString(focus, "toChars"); - return new TypeDetails(CollectionStatus.ORDERED, TypeDetails.FP_String); - } - case Substring : { - checkContextString(focus, "subString"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer)); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); - } - case StartsWith : { - checkContextString(focus, "startsWith"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case EndsWith : { - checkContextString(focus, "endsWith"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case Matches : { - checkContextString(focus, "matches"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case ReplaceMatches : { - checkContextString(focus, "replaceMatches"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); - } - case Contains : { - checkContextString(focus, "contains"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case Replace : { - checkContextString(focus, "replace"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); - } - case Length : { - checkContextPrimitive(focus, "length", false); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); - } - case Children : - return childTypes(focus, "*"); - case Descendants : - return childTypes(focus, "**"); - case MemberOf : { - checkContextCoded(focus, "memberOf"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case Trace : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return focus; - } - case Today : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); - case Now : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); - case Resolve : { - checkContextReference(focus, "resolve"); - return new TypeDetails(CollectionStatus.SINGLETON, "DomainResource"); - } - case Extension : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return new TypeDetails(CollectionStatus.SINGLETON, "Extension"); - } - case HasValue : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case HtmlChecks : - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - case Alias : - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return anything(CollectionStatus.SINGLETON); - case AliasAs : - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String)); - return focus; - case ToInteger : { - checkContextPrimitive(focus, "toInteger", true); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Integer); - } - case ToDecimal : { - checkContextPrimitive(focus, "toDecimal", true); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Decimal); - } - case ToString : { - checkContextPrimitive(focus, "toString", true); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_String); - } - case ToQuantity : { - checkContextPrimitive(focus, "toQuantity", true); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Quantity); - } - case ToBoolean : { - checkContextPrimitive(focus, "toBoolean", false); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case ToDateTime : { - checkContextPrimitive(focus, "toBoolean", false); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_DateTime); - } - case ToTime : { - checkContextPrimitive(focus, "toBoolean", false); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Time); - } - case IsString : - case IsQuantity :{ - checkContextPrimitive(focus, exp.getFunction().toCode(), true); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case IsInteger : - case IsDecimal : - case IsDateTime : - case IsTime : - case IsBoolean : { - checkContextPrimitive(focus, exp.getFunction().toCode(), false); - return new TypeDetails(CollectionStatus.SINGLETON, TypeDetails.FP_Boolean); - } - case Custom : { - return hostServices.checkFunction(context.appInfo, exp.getName(), paramTypes); - } - default: - break; - } - throw new Error("not Implemented yet"); - } - - - private void checkParamTypes(String funcName, List paramTypes, TypeDetails... typeSet) throws PathEngineException { - int i = 0; - for (TypeDetails pt : typeSet) { - if (i == paramTypes.size()) - return; - TypeDetails actual = paramTypes.get(i); - i++; - for (String a : actual.getTypes()) { - if (!pt.hasType(worker, a)) - throw new PathEngineException("The parameter type '"+a+"' is not legal for "+funcName+" parameter "+Integer.toString(i)+". expecting "+pt.toString()); - } - } - } - - private void checkOrdered(TypeDetails focus, String name) throws PathEngineException { - if (focus.getCollectionStatus() == CollectionStatus.UNORDERED) - throw new PathEngineException("The function '"+name+"'() can only be used on ordered collections"); - } - - private void checkContextReference(TypeDetails focus, String name) throws PathEngineException { - if (!focus.hasType(worker, "string") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Reference") && !focus.hasType(worker, "canonical")) - throw new PathEngineException("The function '"+name+"'() can only be used on string, uri, canonical, Reference"); - } - - - private void checkContextCoded(TypeDetails focus, String name) throws PathEngineException { - if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Coding") && !focus.hasType(worker, "CodeableConcept")) - throw new PathEngineException("The function '"+name+"'() can only be used on string, code, uri, Coding, CodeableConcept"); - } - - - private void checkContextString(TypeDetails focus, String name) throws PathEngineException { - if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "id")) - throw new PathEngineException("The function '"+name+"'() can only be used on string, uri, code, id, but found "+focus.describe()); - } - - - private void checkContextPrimitive(TypeDetails focus, String name, boolean canQty) throws PathEngineException { - if (canQty) { - if (!focus.hasType(primitiveTypes) && !focus.hasType("Quantity")) - throw new PathEngineException("The function '"+name+"'() can only be used on a Quantity or on "+primitiveTypes.toString()); - } else if (!focus.hasType(primitiveTypes)) - throw new PathEngineException("The function '"+name+"'() can only be used on "+primitiveTypes.toString()); - } - - - private TypeDetails childTypes(TypeDetails focus, String mask) throws PathEngineException, DefinitionException { - TypeDetails result = new TypeDetails(CollectionStatus.UNORDERED); - for (String f : focus.getTypes()) - getChildTypesByName(f, mask, result); - return result; - } - - private TypeDetails anything(CollectionStatus status) { - return new TypeDetails(status, allTypes.keySet()); - } - - // private boolean isPrimitiveType(String s) { - // return s.equals("boolean") || s.equals("integer") || s.equals("decimal") || s.equals("base64Binary") || s.equals("instant") || s.equals("string") || s.equals("uri") || s.equals("date") || s.equals("dateTime") || s.equals("time") || s.equals("code") || s.equals("oid") || s.equals("id") || s.equals("unsignedInt") || s.equals("positiveInt") || s.equals("markdown"); - // } - - private List evaluateFunction(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - switch (exp.getFunction()) { - case Empty : return funcEmpty(context, focus, exp); - case Not : return funcNot(context, focus, exp); - case Exists : return funcExists(context, focus, exp); - case SubsetOf : return funcSubsetOf(context, focus, exp); - case SupersetOf : return funcSupersetOf(context, focus, exp); - case IsDistinct : return funcIsDistinct(context, focus, exp); - case Distinct : return funcDistinct(context, focus, exp); - case Count : return funcCount(context, focus, exp); - case Where : return funcWhere(context, focus, exp); - case Select : return funcSelect(context, focus, exp); - case All : return funcAll(context, focus, exp); - case Repeat : return funcRepeat(context, focus, exp); - case Aggregate : return funcAggregate(context, focus, exp); - case Item : return funcItem(context, focus, exp); - case As : return funcAs(context, focus, exp); - case OfType : return funcAs(context, focus, exp); - case Type : return funcType(context, focus, exp); - case Is : return funcIs(context, focus, exp); - case Single : return funcSingle(context, focus, exp); - case First : return funcFirst(context, focus, exp); - case Last : return funcLast(context, focus, exp); - case Tail : return funcTail(context, focus, exp); - case Skip : return funcSkip(context, focus, exp); - case Take : return funcTake(context, focus, exp); - case Union : return funcUnion(context, focus, exp); - case Combine : return funcCombine(context, focus, exp); - case Intersect : return funcIntersect(context, focus, exp); - case Exclude : return funcExclude(context, focus, exp); - case Iif : return funcIif(context, focus, exp); - case Lower : return funcLower(context, focus, exp); - case Upper : return funcUpper(context, focus, exp); - case ToChars : return funcToChars(context, focus, exp); - case Substring : return funcSubstring(context, focus, exp); - case StartsWith : return funcStartsWith(context, focus, exp); - case EndsWith : return funcEndsWith(context, focus, exp); - case Matches : return funcMatches(context, focus, exp); - case ReplaceMatches : return funcReplaceMatches(context, focus, exp); - case Contains : return funcContains(context, focus, exp); - case Replace : return funcReplace(context, focus, exp); - case Length : return funcLength(context, focus, exp); - case Children : return funcChildren(context, focus, exp); - case Descendants : return funcDescendants(context, focus, exp); - case MemberOf : return funcMemberOf(context, focus, exp); - case Trace : return funcTrace(context, focus, exp); - case Today : return funcToday(context, focus, exp); - case Now : return funcNow(context, focus, exp); - case Resolve : return funcResolve(context, focus, exp); - case Extension : return funcExtension(context, focus, exp); - case HasValue : return funcHasValue(context, focus, exp); - case AliasAs : return funcAliasAs(context, focus, exp); - case Alias : return funcAlias(context, focus, exp); - case HtmlChecks : return funcHtmlChecks(context, focus, exp); - case ToInteger : return funcToInteger(context, focus, exp); - case ToDecimal : return funcToDecimal(context, focus, exp); - case ToString : return funcToString(context, focus, exp); - case ToBoolean : return funcToBoolean(context, focus, exp); - case ToQuantity : return funcToQuantity(context, focus, exp); - case ToDateTime : return funcToDateTime(context, focus, exp); - case ToTime : return funcToTime(context, focus, exp); - case IsInteger : return funcIsInteger(context, focus, exp); - case IsDecimal : return funcIsDecimal(context, focus, exp); - case IsString : return funcIsString(context, focus, exp); - case IsBoolean : return funcIsBoolean(context, focus, exp); - case IsQuantity : return funcIsQuantity(context, focus, exp); - case IsDateTime : return funcIsDateTime(context, focus, exp); - case IsTime : return funcIsTime(context, focus, exp); - case Custom: { - List> params = new ArrayList>(); - for (ExpressionNode p : exp.getParameters()) - params.add(execute(context, focus, p, true)); - return hostServices.executeFunction(context.appInfo, exp.getName(), params); - } - default: - throw new Error("not Implemented yet"); - } - } - - private List funcAliasAs(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List nl = execute(context, focus, exp.getParameters().get(0), true); - String name = nl.get(0).primitiveValue(); - context.addAlias(name, focus); - return focus; - } - - private List funcAlias(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List nl = execute(context, focus, exp.getParameters().get(0), true); - String name = nl.get(0).primitiveValue(); - List res = new ArrayList(); - Base b = context.getAlias(name); - if (b != null) - res.add(b); - return res; - } - - private List funcHtmlChecks(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - // todo: actually check the HTML - return makeBoolean(true); - } - - - private List funcAll(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - if (exp.getParameters().size() == 1) { - List result = new ArrayList(); - List pc = new ArrayList(); - boolean all = true; - for (Base item : focus) { - pc.clear(); - pc.add(item); - if (!convertToBoolean(execute(changeThis(context, item), pc, exp.getParameters().get(0), true))) { - all = false; - break; - } - } - result.add(new BooleanType(all).noExtensions()); - return result; - } else {// (exp.getParameters().size() == 0) { - List result = new ArrayList(); - boolean all = true; - for (Base item : focus) { - boolean v = false; - if (item instanceof BooleanType) { - v = ((BooleanType) item).booleanValue(); - } else - v = item != null; - if (!v) { - all = false; - break; - } - } - result.add(new BooleanType(all).noExtensions()); - return result; - } - } - - - private ExecutionContext changeThis(ExecutionContext context, Base newThis) { - return new ExecutionContext(context.appInfo, context.resource, context.context, context.aliases, newThis); - } - - private ExecutionTypeContext changeThis(ExecutionTypeContext context, TypeDetails newThis) { - return new ExecutionTypeContext(context.appInfo, context.resource, context.context, newThis); - } - - - private List funcNow(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(DateTimeType.now()); - return result; - } - - - private List funcToday(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new DateType(new Date(), TemporalPrecisionEnum.DAY)); - return result; - } - - - private List funcMemberOf(ExecutionContext context, List focus, ExpressionNode exp) { - throw new Error("not Implemented yet"); - } - - - private List funcDescendants(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List current = new ArrayList(); - current.addAll(focus); - List added = new ArrayList(); - boolean more = true; - while (more) { - added.clear(); - for (Base item : current) { - getChildrenByName(item, "*", added); - } - more = !added.isEmpty(); - result.addAll(added); - current.clear(); - current.addAll(added); - } - return result; - } - - - private List funcChildren(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - for (Base b : focus) - getChildrenByName(b, "*", result); - return result; - } - - - private List funcReplace(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException, PathEngineException { - List result = new ArrayList(); - - if (focus.size() == 1) { - String f = convertToString(focus.get(0)); - - if (!Utilities.noString(f)) { - - if (exp.getParameters().size() != 2) { - - String t = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - String r = convertToString(execute(context, focus, exp.getParameters().get(1), true)); - - String n = f.replace(t, r); - result.add(new StringType(n)); - } - else { - throw new PathEngineException(String.format("funcReplace() : checking for 2 arguments (pattern, substitution) but found %d items", exp.getParameters().size())); - } - } - else { - throw new PathEngineException(String.format("funcReplace() : checking for 1 string item but found empty item")); - } - } - else { - throw new PathEngineException(String.format("funcReplace() : checking for 1 string item but found %d items", focus.size())); - } - return result; - } - - - private List funcReplaceMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) - result.add(new BooleanType(convertToString(focus.get(0)).contains(sw)).noExtensions()); - else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - - private List funcEndsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) - result.add(new BooleanType(convertToString(focus.get(0)).endsWith(sw)).noExtensions()); - else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - - private List funcToString(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new StringType(convertToString(focus)).noExtensions()); - return result; - } - - private List funcToBoolean(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() == 1) { - if (focus.get(0) instanceof BooleanType) - result.add(focus.get(0)); - else if (focus.get(0) instanceof IntegerType) - result.add(new BooleanType(!focus.get(0).primitiveValue().equals("0")).noExtensions()); - else if (focus.get(0) instanceof StringType) { - if ("true".equals(focus.get(0).primitiveValue())) - result.add(new BooleanType(true).noExtensions()); - else if ("false".equals(focus.get(0).primitiveValue())) - result.add(new BooleanType(false).noExtensions()); - } - } - return result; - } - - private List funcToQuantity(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() == 1) { - if (focus.get(0) instanceof Quantity) - result.add(focus.get(0)); - else if (focus.get(0) instanceof StringType) { - Quantity q = parseQuantityString(focus.get(0).primitiveValue()); - if (q != null) - result.add(q.noExtensions()); - } else if (focus.get(0) instanceof IntegerType) { - result.add(new Quantity().setValue(new BigDecimal(focus.get(0).primitiveValue())).setSystem("http://unitsofmeasure.org").setCode("1").noExtensions()); - } else if (focus.get(0) instanceof DecimalType) { - result.add(new Quantity().setValue(new BigDecimal(focus.get(0).primitiveValue())).setSystem("http://unitsofmeasure.org").setCode("1").noExtensions()); - } - } - return result; - } - - private List funcToDateTime(ExecutionContext context, List focus, ExpressionNode exp) { -// List result = new ArrayList(); -// result.add(new BooleanType(convertToBoolean(focus))); -// return result; - throw new NotImplementedException("funcToDateTime is not implemented"); -} - - private List funcToTime(ExecutionContext context, List focus, ExpressionNode exp) { -// List result = new ArrayList(); -// result.add(new BooleanType(convertToBoolean(focus))); -// return result; - throw new NotImplementedException("funcToTime is not implemented"); -} - - - private List funcToDecimal(ExecutionContext context, List focus, ExpressionNode exp) { - String s = convertToString(focus); - List result = new ArrayList(); - if (Utilities.isDecimal(s)) - result.add(new DecimalType(s).noExtensions()); - if ("true".equals(s)) - result.add(new DecimalType(1).noExtensions()); - if ("false".equals(s)) - result.add(new DecimalType(0).noExtensions()); - return result; - } - - - private List funcIif(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List n1 = execute(context, focus, exp.getParameters().get(0), true); - Boolean v = convertToBoolean(n1); - - if (v) - return execute(context, focus, exp.getParameters().get(1), true); - else if (exp.getParameters().size() < 3) - return new ArrayList(); - else - return execute(context, focus, exp.getParameters().get(2), true); - } - - - private List funcTake(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List n1 = execute(context, focus, exp.getParameters().get(0), true); - int i1 = Integer.parseInt(n1.get(0).primitiveValue()); - - List result = new ArrayList(); - for (int i = 0; i < Math.min(focus.size(), i1); i++) - result.add(focus.get(i)); - return result; - } - - - private List funcUnion(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - for (Base item : focus) { - if (!doContains(result, item)) - result.add(item); - } - for (Base item : execute(context, focus, exp.getParameters().get(0), true)) { - if (!doContains(result, item)) - result.add(item); - } - return result; - } - - private List funcCombine(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - for (Base item : focus) { - result.add(item); - } - for (Base item : execute(context, focus, exp.getParameters().get(0), true)) { - result.add(item); - } - return result; - } - - private List funcIntersect(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List other = execute(context, focus, exp.getParameters().get(0), true); - - for (Base item : focus) { - if (!doContains(result, item) && doContains(other, item)) - result.add(item); - } - return result; - } - - private List funcExclude(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List other = execute(context, focus, exp.getParameters().get(0), true); - - for (Base item : focus) { - if (!doContains(result, item) && !doContains(other, item)) - result.add(item); - } - return result; - } - - - private List funcSingle(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { - if (focus.size() == 1) - return focus; - throw new PathEngineException(String.format("Single() : checking for 1 item but found %d items", focus.size())); - } - - - private List funcIs(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { - if (focus.size() == 0 || focus.size() > 1) - return makeBoolean(false); - String ns = null; - String n = null; - - ExpressionNode texp = exp.getParameters().get(0); - if (texp.getKind() != Kind.Name) - throw new PathEngineException("Unsupported Expression type for Parameter on Is"); - if (texp.getInner() != null) { - if (texp.getInner().getKind() != Kind.Name) - throw new PathEngineException("Unsupported Expression type for Parameter on Is"); - ns = texp.getName(); - n = texp.getInner().getName(); - } else if (Utilities.existsInList(texp.getName(), "Boolean", "Integer", "Decimal", "String", "DateTime", "Time", "SimpleTypeInfo", "ClassInfo")) { - ns = "System"; - n = texp.getName(); - } else { - ns = "FHIR"; - n = texp.getName(); - } - if (ns.equals("System")) { - if (!(focus.get(0) instanceof Element) || ((Element) focus.get(0)).isDisallowExtensions()) - return makeBoolean(n.equals(Utilities.capitalize(focus.get(0).fhirType()))); - else - return makeBoolean(false); - } else if (ns.equals("FHIR")) { - return makeBoolean(n.equals(focus.get(0).fhirType())); - } else { - return makeBoolean(false); - } - } - - - private List funcAs(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - String tn = exp.getParameters().get(0).getName(); - for (Base b : focus) - if (b.hasType(tn)) - result.add(b); - return result; - } - - private List funcType(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - for (Base item : focus) - result.add(new ClassTypeInfo(item)); - return result; + private String tailDot(String path) { + return path.substring(path.lastIndexOf(".") + 1); } + private boolean tailMatches(ElementDefinition t, String d) { + String tail = tailDot(t.getPath()); + if (d.contains("[")) + return tail.startsWith(d.substring(0, d.indexOf('['))); + else if (tail.equals(d)) + return true; + else if (t.getType().size() == 1 && t.getPath().toUpperCase().endsWith(t.getType().get(0).getCode().toUpperCase())) + return tail.startsWith(d); - private List funcRepeat(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List current = new ArrayList(); - current.addAll(focus); - List added = new ArrayList(); - boolean more = true; - while (more) { - added.clear(); - List pc = new ArrayList(); - for (Base item : current) { - pc.clear(); - pc.add(item); - added.addAll(execute(changeThis(context, item), pc, exp.getParameters().get(0), false)); - } - more = !added.isEmpty(); - result.addAll(added); - current.clear(); - current.addAll(added); - } - return result; - } - - - private List funcAggregate(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List total = new ArrayList(); - if (exp.parameterCount() > 1) - total = execute(context, focus, exp.getParameters().get(1), false); - - List pc = new ArrayList(); - for (Base item : focus) { - ExecutionContext c = changeThis(context, item); - c.total = total; - total = execute(c, pc, exp.getParameters().get(0), true); - } - return total; - } - - - - private List funcIsDistinct(ExecutionContext context, List focus, ExpressionNode exp) { - if (focus.size() <= 1) - return makeBoolean(true); - - boolean distinct = true; - for (int i = 0; i < focus.size(); i++) { - for (int j = i+1; j < focus.size(); j++) { - if (doEquals(focus.get(j), focus.get(i))) { - distinct = false; - break; - } - } - } - return makeBoolean(distinct); - } - - - private List funcSupersetOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List target = execute(context, focus, exp.getParameters().get(0), true); - - boolean valid = true; - for (Base item : target) { - boolean found = false; - for (Base t : focus) { - if (Base.compareDeep(item, t, false)) { - found = true; - break; - } - } - if (!found) { - valid = false; - break; - } - } - List result = new ArrayList(); - result.add(new BooleanType(valid).noExtensions()); - return result; - } - - - private List funcSubsetOf(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List target = execute(context, focus, exp.getParameters().get(0), true); - - boolean valid = true; - for (Base item : focus) { - boolean found = false; - for (Base t : target) { - if (Base.compareDeep(item, t, false)) { - found = true; - break; - } - } - if (!found) { - valid = false; - break; - } - } - List result = new ArrayList(); - result.add(new BooleanType(valid).noExtensions()); - return result; - } - - - private List funcExists(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new BooleanType(!ElementUtil.isEmpty(focus)).noExtensions()); - return result; - } - - - private List funcResolve(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - for (Base item : focus) { - if (hostServices != null) { - String s = convertToString(item); - if (item.fhirType().equals("Reference")) { - Property p = item.getChildByName("reference"); - if (p != null && p.hasValues()) - s = convertToString(p.getValues().get(0)); - else - s = null; // a reference without any valid actual reference (just identifier or display, but we can't resolve it) - } - if (item.fhirType().equals("canonical")) { - s = item.primitiveValue(); - } - if (s != null) { - Base res = null; - if (s.startsWith("#")) { - String id = s.substring(1); - Property p = context.resource.getChildByName("contained"); - for (Base c : p.getValues()) { - if (id.equals(c.getIdBase())) { - res = c; - break; - } - } - } else - res = hostServices.resolveReference(context.appInfo, s); - if (res != null) - result.add(res); - } - } - } - return result; - } - - private List funcExtension(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List nl = execute(context, focus, exp.getParameters().get(0), true); - String url = nl.get(0).primitiveValue(); - - for (Base item : focus) { - List ext = new ArrayList(); - getChildrenByName(item, "extension", ext); - getChildrenByName(item, "modifierExtension", ext); - for (Base ex : ext) { - List vl = new ArrayList(); - getChildrenByName(ex, "url", vl); - if (convertToString(vl).equals(url)) - result.add(ex); - } - } - return result; - } - - private List funcTrace(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List nl = execute(context, focus, exp.getParameters().get(0), true); - String name = nl.get(0).primitiveValue(); - - log(name, focus); - return focus; - } - - private List funcDistinct(ExecutionContext context, List focus, ExpressionNode exp) { - if (focus.size() <= 1) - return focus; - - List result = new ArrayList(); - for (int i = 0; i < focus.size(); i++) { - boolean found = false; - for (int j = i+1; j < focus.size(); j++) { - if (doEquals(focus.get(j), focus.get(i))) { - found = true; - break; - } - } - if (!found) - result.add(focus.get(i)); - } - return result; - } - - private List funcMatches(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) { - String st = convertToString(focus.get(0)); - if (Utilities.noString(st)) - result.add(new BooleanType(false).noExtensions()); - else - result.add(new BooleanType(st.matches(sw)).noExtensions()); - } else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - private List funcContains(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) { - String st = convertToString(focus.get(0)); - if (Utilities.noString(st)) - result.add(new BooleanType(false).noExtensions()); - else - result.add(new BooleanType(st.contains(sw)).noExtensions()); - } else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - private List funcLength(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() == 1) { - String s = convertToString(focus.get(0)); - result.add(new IntegerType(s.length()).noExtensions()); - } - return result; - } - - private List funcHasValue(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() == 1) { - String s = convertToString(focus.get(0)); - result.add(new BooleanType(!Utilities.noString(s)).noExtensions()); - } - return result; - } - - private List funcStartsWith(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String sw = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - - if (focus.size() == 1 && !Utilities.noString(sw)) - result.add(new BooleanType(convertToString(focus.get(0)).startsWith(sw)).noExtensions()); - else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - private List funcLower(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - if (focus.size() == 1) { - String s = convertToString(focus.get(0)); - if (!Utilities.noString(s)) - result.add(new StringType(s.toLowerCase()).noExtensions()); - } - return result; - } - - private List funcUpper(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - if (focus.size() == 1) { - String s = convertToString(focus.get(0)); - if (!Utilities.noString(s)) - result.add(new StringType(s.toUpperCase()).noExtensions()); - } - return result; - } - - private List funcToChars(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - if (focus.size() == 1) { - String s = convertToString(focus.get(0)); - for (char c : s.toCharArray()) - result.add(new StringType(String.valueOf(c)).noExtensions()); - } - return result; - } - - private List funcSubstring(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List n1 = execute(context, focus, exp.getParameters().get(0), true); - int i1 = Integer.parseInt(n1.get(0).primitiveValue()); - int i2 = -1; - if (exp.parameterCount() == 2) { - List n2 = execute(context, focus, exp.getParameters().get(1), true); - i2 = Integer.parseInt(n2.get(0).primitiveValue()); - } - - if (focus.size() == 1) { - String sw = convertToString(focus.get(0)); - String s; - if (i1 < 0 || i1 >= sw.length()) - return new ArrayList(); - if (exp.parameterCount() == 2) - s = sw.substring(i1, Math.min(sw.length(), i1+i2)); - else - s = sw.substring(i1); - if (!Utilities.noString(s)) - result.add(new StringType(s).noExtensions()); - } - return result; - } - - private List funcToInteger(ExecutionContext context, List focus, ExpressionNode exp) { - String s = convertToString(focus); - List result = new ArrayList(); - if (Utilities.isInteger(s)) - result.add(new IntegerType(s).noExtensions()); - else if ("true".equals(s)) - result.add(new IntegerType(1).noExtensions()); - else if ("false".equals(s)) - result.add(new IntegerType(0).noExtensions()); - return result; - } - - private List funcIsInteger(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() != 1) - result.add(new BooleanType(false).noExtensions()); - else if (focus.get(0) instanceof IntegerType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof BooleanType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof StringType) - result.add(new BooleanType(Utilities.isInteger(convertToString(focus.get(0)))).noExtensions()); - else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - private List funcIsBoolean(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() != 1) - result.add(new BooleanType(false).noExtensions()); - else if (focus.get(0) instanceof IntegerType && ((IntegerType) focus.get(0)).getValue() >= 0) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof BooleanType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof StringType) - result.add(new BooleanType(Utilities.existsInList(convertToString(focus.get(0)), "true", "false")).noExtensions()); - else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - private List funcIsDateTime(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() != 1) - result.add(new BooleanType(false).noExtensions()); - else if (focus.get(0) instanceof DateTimeType || focus.get(0) instanceof DateType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof StringType) - result.add(new BooleanType((convertToString(focus.get(0)).matches - ("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?"))).noExtensions()); - else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - private List funcIsTime(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() != 1) - result.add(new BooleanType(false).noExtensions()); - else if (focus.get(0) instanceof TimeType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof StringType) - result.add(new BooleanType((convertToString(focus.get(0)).matches - ("T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?"))).noExtensions()); - else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - private List funcIsString(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() != 1) - result.add(new BooleanType(false).noExtensions()); - else if (!(focus.get(0) instanceof DateTimeType) && !(focus.get(0) instanceof TimeType)) - result.add(new BooleanType(true).noExtensions()); - else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - private List funcIsQuantity(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() != 1) - result.add(new BooleanType(false).noExtensions()); - else if (focus.get(0) instanceof IntegerType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof DecimalType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof Quantity) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof StringType) { - Quantity q = parseQuantityString(focus.get(0).primitiveValue()); - result.add(new BooleanType(q != null).noExtensions()); - } else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - public Quantity parseQuantityString(String s) { - if (s == null) - return null; - s = s.trim(); - if (s.contains(" ")) { - String v = s.substring(0, s.indexOf(" ")).trim(); - s = s.substring(s.indexOf(" ")).trim(); - if (!Utilities.isDecimal(v)) - return null; - if (s.startsWith("'") && s.endsWith("'")) - return Quantity.fromUcum(v, s.substring(1, s.length()-1)); - if (s.equals("year") || s.equals("years")) - return Quantity.fromUcum(v, "a"); - else if (s.equals("month") || s.equals("months")) - return Quantity.fromUcum(v, "mo"); - else if (s.equals("week") || s.equals("weeks")) - return Quantity.fromUcum(v, "wk"); - else if (s.equals("day") || s.equals("days")) - return Quantity.fromUcum(v, "d"); - else if (s.equals("hour") || s.equals("hours")) - return Quantity.fromUcum(v, "h"); - else if (s.equals("minute") || s.equals("minutes")) - return Quantity.fromUcum(v, "min"); - else if (s.equals("second") || s.equals("seconds")) - return Quantity.fromUcum(v, "s"); - else if (s.equals("millisecond") || s.equals("milliseconds")) - return Quantity.fromUcum(v, "ms"); - else - return null; - } else { - if (Utilities.isDecimal(s)) - return new Quantity().setValue(new BigDecimal(s)).setSystem("http://unitsofmeasure.org").setCode("1"); - else - return null; - } - } - - - private List funcIsDecimal(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() != 1) - result.add(new BooleanType(false).noExtensions()); - else if (focus.get(0) instanceof IntegerType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof BooleanType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof DecimalType) - result.add(new BooleanType(true).noExtensions()); - else if (focus.get(0) instanceof StringType) - result.add(new BooleanType(Utilities.isDecimal(convertToString(focus.get(0)))).noExtensions()); - else - result.add(new BooleanType(false).noExtensions()); - return result; - } - - private List funcCount(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new IntegerType(focus.size()).noExtensions()); - return result; - } - - private List funcSkip(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List n1 = execute(context, focus, exp.getParameters().get(0), true); - int i1 = Integer.parseInt(n1.get(0).primitiveValue()); - - List result = new ArrayList(); - for (int i = i1; i < focus.size(); i++) - result.add(focus.get(i)); - return result; - } - - private List funcTail(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - for (int i = 1; i < focus.size(); i++) - result.add(focus.get(i)); - return result; - } - - private List funcLast(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() > 0) - result.add(focus.get(focus.size()-1)); - return result; - } - - private List funcFirst(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - if (focus.size() > 0) - result.add(focus.get(0)); - return result; - } - - - private List funcWhere(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List pc = new ArrayList(); - for (Base item : focus) { - pc.clear(); - pc.add(item); - if (convertToBoolean(execute(changeThis(context, item), pc, exp.getParameters().get(0), true))) - result.add(item); - } - return result; - } - - private List funcSelect(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - List pc = new ArrayList(); - for (Base item : focus) { - pc.clear(); - pc.add(item); - result.addAll(execute(changeThis(context, item), pc, exp.getParameters().get(0), true)); - } - return result; - } - - - private List funcItem(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { - List result = new ArrayList(); - String s = convertToString(execute(context, focus, exp.getParameters().get(0), true)); - if (Utilities.isInteger(s) && Integer.parseInt(s) < focus.size()) - result.add(focus.get(Integer.parseInt(s))); - return result; - } - - private List funcEmpty(ExecutionContext context, List focus, ExpressionNode exp) { - List result = new ArrayList(); - result.add(new BooleanType(ElementUtil.isEmpty(focus)).noExtensions()); - return result; - } - - private List funcNot(ExecutionContext context, List focus, ExpressionNode exp) { - return makeBoolean(!convertToBoolean(focus)); - } - - public class ElementDefinitionMatch { - private ElementDefinition definition; - private String fixedType; - public ElementDefinitionMatch(ElementDefinition definition, String fixedType) { - super(); - this.definition = definition; - this.fixedType = fixedType; - } - public ElementDefinition getDefinition() { - return definition; - } - public String getFixedType() { - return fixedType; - } - - } - - private void getChildTypesByName(String type, String name, TypeDetails result) throws PathEngineException, DefinitionException { - if (Utilities.noString(type)) - throw new PathEngineException("No type provided in BuildToolPathEvaluator.getChildTypesByName"); - if (type.equals("http://hl7.org/fhir/StructureDefinition/xhtml")) - return; - if (type.equals(TypeDetails.FP_SimpleTypeInfo)) { - getSimpleTypeChildTypesByName(name, result); - } else if (type.equals(TypeDetails.FP_ClassInfo)) { - getClassInfoChildTypesByName(name, result); - } else { - String url = null; - if (type.contains("#")) { - url = type.substring(0, type.indexOf("#")); - } else { - url = type; - } - String tail = ""; - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, url); - if (sd == null) - throw new DefinitionException("Unknown type "+type); // this really is an error, because we can only get to here if the internal infrastrucgture is wrong - List sdl = new ArrayList(); - ElementDefinitionMatch m = null; - if (type.contains("#")) - m = getElementDefinition(sd, type.substring(type.indexOf("#")+1), false); - if (m != null && hasDataType(m.definition)) { - if (m.fixedType != null) - { - StructureDefinition dt = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(m.fixedType)); - if (dt == null) - throw new DefinitionException("unknown data type "+m.fixedType); - sdl.add(dt); - } else - for (TypeRefComponent t : m.definition.getType()) { - StructureDefinition dt = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(t.getCode())); - if (dt == null) - throw new DefinitionException("unknown data type "+t.getCode()); - sdl.add(dt); - } - } else { - sdl.add(sd); - if (type.contains("#")) { - tail = type.substring(type.indexOf("#")+1); - tail = tail.substring(tail.indexOf(".")); - } - } - - for (StructureDefinition sdi : sdl) { - String path = sdi.getSnapshot().getElement().get(0).getPath()+tail+"."; - if (name.equals("**")) { - assert(result.getCollectionStatus() == CollectionStatus.UNORDERED); - for (ElementDefinition ed : sdi.getSnapshot().getElement()) { - if (ed.getPath().startsWith(path)) - for (TypeRefComponent t : ed.getType()) { - if (t.hasCode() && t.getCodeElement().hasValue()) { - String tn = null; - if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) - tn = sdi.getType()+"#"+ed.getPath(); - else - tn = t.getCode(); - if (t.getCode().equals("Resource")) { - for (String rn : worker.getResourceNames()) { - if (!result.hasType(worker, rn)) { - getChildTypesByName(result.addType(rn), "**", result); - } - } - } else if (!result.hasType(worker, tn)) { - getChildTypesByName(result.addType(tn), "**", result); - } - } - } - } - } else if (name.equals("*")) { - assert(result.getCollectionStatus() == CollectionStatus.UNORDERED); - for (ElementDefinition ed : sdi.getSnapshot().getElement()) { - if (ed.getPath().startsWith(path) && !ed.getPath().substring(path.length()).contains(".")) - for (TypeRefComponent t : ed.getType()) { - if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) - result.addType(sdi.getType()+"#"+ed.getPath()); - else if (t.getCode().equals("Resource")) - result.addTypes(worker.getResourceNames()); - else - result.addType(t.getCode()); - } - } - } else { - path = sdi.getSnapshot().getElement().get(0).getPath()+tail+"."+name; - - ElementDefinitionMatch ed = getElementDefinition(sdi, path, false); - if (ed != null) { - if (!Utilities.noString(ed.getFixedType())) - result.addType(ed.getFixedType()); - else - for (TypeRefComponent t : ed.getDefinition().getType()) { - if (Utilities.noString(t.getCode())) - break; // throw new PathEngineException("Illegal reference to primitive value attribute @ "+path); - - ProfiledType pt = null; - if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) - pt = new ProfiledType(sdi.getUrl()+"#"+path); - else if (t.getCode().equals("Resource")) - result.addTypes(worker.getResourceNames()); - else - pt = new ProfiledType(t.getCode()); - if (pt != null) { - if (t.hasProfile()) - pt.addProfiles(t.getProfile()); - if (ed.getDefinition().hasBinding()) - pt.addBinding(ed.getDefinition().getBinding()); - result.addType(pt); - } - } - } - } - } - } - } - - private void getClassInfoChildTypesByName(String name, TypeDetails result) { - if (name.equals("namespace")) - result.addType(TypeDetails.FP_String); - if (name.equals("name")) - result.addType(TypeDetails.FP_String); - } - - - private void getSimpleTypeChildTypesByName(String name, TypeDetails result) { - if (name.equals("namespace")) - result.addType(TypeDetails.FP_String); - if (name.equals("name")) - result.addType(TypeDetails.FP_String); - } - - - private ElementDefinitionMatch getElementDefinition(StructureDefinition sd, String path, boolean allowTypedName) throws PathEngineException { - for (ElementDefinition ed : sd.getSnapshot().getElement()) { - if (ed.getPath().equals(path)) { - if (ed.hasContentReference()) { - return getElementDefinitionById(sd, ed.getContentReference()); - } else - return new ElementDefinitionMatch(ed, null); - } - if (ed.getPath().endsWith("[x]") && path.startsWith(ed.getPath().substring(0, ed.getPath().length()-3)) && path.length() == ed.getPath().length()-3) - return new ElementDefinitionMatch(ed, null); - if (allowTypedName && ed.getPath().endsWith("[x]") && path.startsWith(ed.getPath().substring(0, ed.getPath().length()-3)) && path.length() > ed.getPath().length()-3) { - String s = Utilities.uncapitalize(path.substring(ed.getPath().length()-3)); - if (primitiveTypes.contains(s)) - return new ElementDefinitionMatch(ed, s); - else - return new ElementDefinitionMatch(ed, path.substring(ed.getPath().length()-3)); - } - if (ed.getPath().contains(".") && path.startsWith(ed.getPath()+".") && (ed.getType().size() > 0) && !isAbstractType(ed.getType())) { - // now we walk into the type. - if (ed.getType().size() > 1) // if there's more than one type, the test above would fail this - throw new PathEngineException("Internal typing issue...."); - StructureDefinition nsd = worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(ed.getType().get(0).getCode())); - if (nsd == null) - throw new PathEngineException("Unknown type "+ed.getType().get(0).getCode()); - return getElementDefinition(nsd, nsd.getId()+path.substring(ed.getPath().length()), allowTypedName); - } - if (ed.hasContentReference() && path.startsWith(ed.getPath()+".")) { - ElementDefinitionMatch m = getElementDefinitionById(sd, ed.getContentReference()); - return getElementDefinition(sd, m.definition.getPath()+path.substring(ed.getPath().length()), allowTypedName); - } - } - return null; - } - - private boolean isAbstractType(List list) { - return list.size() != 1 ? true : Utilities.existsInList(list.get(0).getCode(), "Element", "BackboneElement", "Resource", "DomainResource"); -} - - - private boolean hasType(ElementDefinition ed, String s) { - for (TypeRefComponent t : ed.getType()) - if (s.equalsIgnoreCase(t.getCode())) - return true; return false; } - private boolean hasDataType(ElementDefinition ed) { - return ed.hasType() && !(ed.getType().get(0).getCode().equals("Element") || ed.getType().get(0).getCode().equals("BackboneElement")); - } - - private ElementDefinitionMatch getElementDefinitionById(StructureDefinition sd, String ref) { - for (ElementDefinition ed : sd.getSnapshot().getElement()) { - if (ref.equals("#"+ed.getId())) - return new ElementDefinitionMatch(ed, null); - } - return null; - } - - - public boolean hasLog() { - return log != null && log.length() > 0; - } - - public String takeLog() { if (!hasLog()) return ""; @@ -3632,111 +3589,270 @@ public class FHIRPathEngine { } - /** given an element definition in a profile, what element contains the differentiating fixed - * for the element, given the differentiating expresssion. The expression is only allowed to - * use a subset of FHIRPath - * - * @param profile - * @param element - * @return - * @throws PathEngineException - * @throws DefinitionException - */ - public ElementDefinition evaluateDefinition(ExpressionNode expr, StructureDefinition profile, ElementDefinition element) throws DefinitionException { - StructureDefinition sd = profile; - ElementDefinition focus = null; + // if the fhir path expressions are allowed to use constants beyond those defined in the specification + // the application can implement them by providing a constant resolver + public interface IEvaluationContext { + /** + * Check the function parameters, and throw an error if they are incorrect, or return the type for the function + * + * @param functionName + * @param parameters + * @return + */ + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException; - if (expr.getKind() == Kind.Name) { - List childDefinitions; - childDefinitions = ProfileUtilities.getChildMap(sd, element); - // if that's empty, get the children of the type - if (childDefinitions.isEmpty()) { - sd = fetchStructureByType(element); - if (sd == null) - throw new DefinitionException("Problem with use of resolve() - profile '"+element.getType().get(0).getProfile()+"' on "+element.getId()+" could not be resolved"); - childDefinitions = ProfileUtilities.getChildMap(sd, sd.getSnapshot().getElementFirstRep()); + /** + * @param appContext + * @param functionName + * @param parameters + * @return + */ + public List executeFunction(Object appContext, String functionName, List> parameters); + + /** + * when the .log() function is called + * + * @param argument + * @param focus + * @return + */ + public boolean log(String argument, List focus); + + /** + * A constant reference - e.g. a reference to a name that must be resolved in context. + * The % will be removed from the constant name before this is invoked. + *

    + * This will also be called if the host invokes the FluentPath engine with a context of null + * + * @param appContext - content passed into the fluent path engine + * @param name - name reference to resolve + * @return the value of the reference (or null, if it's not valid, though can throw an exception if desired) + */ + public Base resolveConstant(Object appContext, String name) throws PathEngineException; + + // extensibility for functions + + public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException; + + /** + * @param functionName + * @return null if the function is not known + */ + public FunctionDetails resolveFunction(String functionName); + + /** + * Implementation of resolve() function. Passed a string, return matching resource, if one is known - else null + * + * @param url + * @return + * @throws FHIRException + */ + public Base resolveReference(Object appContext, String url) throws FHIRException; + + public class FunctionDetails { + private String description; + private int minParameters; + private int maxParameters; + + public FunctionDetails(String description, int minParameters, int maxParameters) { + super(); + this.description = description; + this.minParameters = minParameters; + this.maxParameters = maxParameters; } - for (ElementDefinition t : childDefinitions) { - if (tailMatches(t, expr.getName())) { - focus = t; - break; - } + + public String getDescription() { + return description; } - } else if (expr.getKind() == Kind.Function) { - if ("resolve".equals(expr.getName())) { - if (!element.hasType()) - throw new DefinitionException("illegal use of resolve() in discriminator - no type on element "+element.getId()); - if (element.getType().size() > 1) - throw new DefinitionException("illegal use of resolve() in discriminator - Multiple possible types on "+element.getId()); - if (!element.getType().get(0).hasTarget()) - throw new DefinitionException("illegal use of resolve() in discriminator - type on "+element.getId()+" is not Reference ("+element.getType().get(0).getCode()+")"); - if (element.getType().get(0).getTargetProfile().size() > 1) - throw new DefinitionException("illegal use of resolve() in discriminator - Multiple possible target type profiles on "+element.getId()); - sd = worker.fetchResource(StructureDefinition.class, element.getType().get(0).getTargetProfile().get(0).getValue()); - if (sd == null) - throw new DefinitionException("Problem with use of resolve() - profile '"+element.getType().get(0).getTargetProfile()+"' on "+element.getId()+" could not be resolved"); - focus = sd.getSnapshot().getElementFirstRep(); - } else if ("extension".equals(expr.getName())) { - String targetUrl = expr.getParameters().get(0).getConstant().primitiveValue(); -// targetUrl = targetUrl.substring(1,targetUrl.length()-1); - List childDefinitions = ProfileUtilities.getChildMap(sd, element); - for (ElementDefinition t : childDefinitions) { - if (t.getPath().endsWith(".extension") && t.hasSliceName()) { - sd = worker.fetchResource(StructureDefinition.class, t.getType().get(0).getProfile().get(0).getValue()); - while (sd!=null && !sd.getBaseDefinition().equals("http://hl7.org/fhir/StructureDefinition/Extension")) - sd = worker.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); - if (sd.getUrl().equals(targetUrl)) { - focus = t; - break; - } - } - } - } else - throw new DefinitionException("illegal function name "+expr.getName()+"() in discriminator"); - } else if (expr.getKind() == Kind.Group) { - throw new DefinitionException("illegal expression syntax in discriminator (group)"); - } else if (expr.getKind() == Kind.Constant) { - throw new DefinitionException("illegal expression syntax in discriminator (const)"); + + public int getMaxParameters() { + return maxParameters; + } + + public int getMinParameters() { + return minParameters; + } + } - if (focus == null) - throw new DefinitionException("Unable to resolve discriminator"); - else if (expr.getInner() == null) - return focus; - else - return evaluateDefinition(expr.getInner(), sd, focus); } - private StructureDefinition fetchStructureByType(ElementDefinition ed) throws DefinitionException { - if (ed.getType().size() == 0) - throw new DefinitionException("Error in discriminator at "+ed.getId()+": no children, no type"); - if (ed.getType().size() > 1) - throw new DefinitionException("Error in discriminator at "+ed.getId()+": no children, multiple types"); - if (ed.getType().get(0).getProfile().size() > 1) - throw new DefinitionException("Error in discriminator at "+ed.getId()+": no children, multiple type profiles"); - if (ed.hasSlicing()) - throw new DefinitionException("Error in discriminator at "+ed.getId()+": slicing found"); - if (ed.getType().get(0).hasProfile()) - return worker.fetchResource(StructureDefinition.class, ed.getType().get(0).getProfile().get(0).getValue()); - else - return worker.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(ed.getType().get(0).getCode())); + private class FHIRConstant extends Base { + + private static final long serialVersionUID = -8933773658248269439L; + private String value; + + public FHIRConstant(String value) { + this.value = value; + } + + @Override + public String fhirType() { + return "%constant"; + } + + @Override + public String getIdBase() { + return null; + } + + @Override + public void setIdBase(String value) { + } + + public String getValue() { + return value; + } + + @Override + protected void listChildren(List result) { + } } + private class ClassTypeInfo extends Base { + private static final long serialVersionUID = 4909223114071029317L; + private Base instance; - private boolean tailMatches(ElementDefinition t, String d) { - String tail = tailDot(t.getPath()); - if (d.contains("[")) - return tail.startsWith(d.substring(0, d.indexOf('['))); - else if (tail.equals(d)) - return true; - else if (t.getType().size()==1 && t.getPath().toUpperCase().endsWith(t.getType().get(0).getCode().toUpperCase())) - return tail.startsWith(d); - - return false; + public ClassTypeInfo(Base instance) { + super(); + this.instance = instance; + } + + @Override + public String fhirType() { + return "ClassInfo"; + } + + @Override + public String getIdBase() { + return null; + } + + @Override + public void setIdBase(String value) { + } + + private String getName() { + if ((instance instanceof Resource)) + return instance.fhirType(); + else if (!(instance instanceof Element) || ((Element) instance).isDisallowExtensions()) + return Utilities.capitalize(instance.fhirType()); + else + return instance.fhirType(); + } + + private String getNamespace() { + if ((instance instanceof Resource)) + return "FHIR"; + else if (!(instance instanceof Element) || ((Element) instance).isDisallowExtensions()) + return "System"; + else + return "FHIR"; + } + + public Base[] getProperty(int hash, String name, boolean checkValid) throws FHIRException { + if (name.equals("name")) + return new Base[] {new StringType(getName())}; + else if (name.equals("namespace")) + return new Base[] {new StringType(getNamespace())}; + else + return super.getProperty(hash, name, checkValid); + } + + @Override + protected void listChildren(List result) { + } } - private String tailDot(String path) { - return path.substring(path.lastIndexOf(".") + 1); + private class ExecutionContext { + private Object appInfo; + private Base resource; + private Base context; + private Base thisItem; + private List total; + private Map aliases; + + public ExecutionContext(Object appInfo, Base resource, Base context, Map aliases, Base thisItem) { + this.appInfo = appInfo; + this.context = context; + this.resource = resource; + this.aliases = aliases; + this.thisItem = thisItem; + } + + public void addAlias(String name, List focus) throws FHIRException { + if (aliases == null) + aliases = new HashMap(); + else + aliases = new HashMap(aliases); // clone it, since it's going to change + if (focus.size() > 1) + throw new FHIRException("Attempt to alias a collection, not a singleton"); + aliases.put(name, focus.size() == 0 ? null : focus.get(0)); + } + + public Base getAlias(String name) { + return aliases == null ? null : aliases.get(name); + } + + public Base getResource() { + return resource; + } + + public Base getThisItem() { + return thisItem; + } + + public List getTotal() { + return total; + } + } + + private class ExecutionTypeContext { + private Object appInfo; + private String resource; + private String context; + private TypeDetails thisItem; + private TypeDetails total; + + + public ExecutionTypeContext(Object appInfo, String resource, String context, TypeDetails thisItem) { + super(); + this.appInfo = appInfo; + this.resource = resource; + this.context = context; + this.thisItem = thisItem; + + } + + public String getResource() { + return resource; + } + + public TypeDetails getThisItem() { + return thisItem; + } + + + } + + public class ElementDefinitionMatch { + private ElementDefinition definition; + private String fixedType; + + public ElementDefinitionMatch(ElementDefinition definition, String fixedType) { + super(); + this.definition = definition; + this.fixedType = fixedType; + } + + public ElementDefinition getDefinition() { + return definition; + } + + public String getFixedType() { + return fixedType; + } + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java index 60c788985dd..8ab4dca6ac0 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java @@ -958,6 +958,8 @@ public class ClientR4Test { } + + @Test public void testSearchWithStringIncludes() throws Exception { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java index edfd12cb03e..1acbbc3cc5e 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.BundleUtil; 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.commons.io.input.ReaderInputStream; import org.apache.commons.lang3.StringUtils; @@ -44,66 +45,67 @@ import java.io.StringReader; import java.nio.charset.Charset; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class GenericClientTest { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GenericClientTest.class); - private static FhirContext ourCtx; - private HttpClient myHttpClient; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GenericClientTest.class); + private static FhirContext ourCtx; + private HttpClient myHttpClient; - private HttpResponse myHttpResponse; + private HttpResponse myHttpResponse; - @Before - public void before() { + @Before + public void before() { - myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); - ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient); - ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); + ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient); + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); - myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); + myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); - System.setProperty(BaseClient.HAPI_CLIENT_KEEPRESPONSES, "true"); - } + System.setProperty(BaseClient.HAPI_CLIENT_KEEPRESPONSES, "true"); + } - private Patient createPatientP1() { - Patient p1 = new Patient(); - p1.addIdentifier().setSystem("foo:bar").setValue("12345"); - p1.addName().setFamily("Smith").addGiven("John"); - return p1; - } + private Patient createPatientP1() { + Patient p1 = new Patient(); + p1.addIdentifier().setSystem("foo:bar").setValue("12345"); + p1.addName().setFamily("Smith").addGiven("John"); + return p1; + } - private Bundle createTransactionBundleInput() { - Bundle input = new Bundle(); - input.setType(BundleType.TRANSACTION); - input - .addEntry() - .setResource(createPatientP1()) - .getRequest() - .setMethod(HTTPVerb.POST); - return input; - } + private Bundle createTransactionBundleInput() { + Bundle input = new Bundle(); + input.setType(BundleType.TRANSACTION); + input + .addEntry() + .setResource(createPatientP1()) + .getRequest() + .setMethod(HTTPVerb.POST); + return input; + } - private Bundle createTransactionBundleOutput() { - Bundle output = new Bundle(); - output.setType(BundleType.TRANSACTIONRESPONSE); - output - .addEntry() - .setResource(createPatientP1()) - .getResponse() - .setLocation(createPatientP1().getId()); - return output; - } + private Bundle createTransactionBundleOutput() { + Bundle output = new Bundle(); + output.setType(BundleType.TRANSACTIONRESPONSE); + output + .addEntry() + .setResource(createPatientP1()) + .getResponse() + .setLocation(createPatientP1().getId()); + return output; + } - private String extractBody(ArgumentCaptor capt, int count) throws IOException { - String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(count)).getEntity().getContent(), "UTF-8"); - return body; - } + private String extractBody(ArgumentCaptor capt, int count) throws IOException { + String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(count)).getEntity().getContent(), "UTF-8"); + return body; + } - private String getPatientFeedWithOneResult() { - return ClientR4Test.getPatientFeedWithOneResult(ourCtx); + private String getPatientFeedWithOneResult() { + return ClientR4Test.getPatientFeedWithOneResult(ourCtx); // //@formatter:off // String msg = "\n" + // "\n" + @@ -127,13 +129,13 @@ public class GenericClientTest { // + " </entry>\n" // + "</feed>"; // //@formatter:on - // return msg; - } + // return msg; + } - private String getResourceResult() { - //@formatter:off + private String getResourceResult() { + //@formatter:off String msg = - "<Patient xmlns=\"http://hl7.org/fhir\">" + "<Patient xmlns=\"http://hl7.org/fhir\">" + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" @@ -143,8 +145,8 @@ public class GenericClientTest { + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" + "</Patient>"; //@formatter:on - return msg; - } + return msg; + } @SuppressWarnings("unused") @Test @@ -221,1478 +223,1509 @@ public class GenericClientTest { assertEquals("no-cache, no-store", capt.getValue().getHeaders("Cache-Control")[0].getValue()); } - @Test - public void testCreatePopulatesIsCreated() throws Exception { + @Test + public void testCreatePopulatesIsCreated() throws Exception { - Patient p1 = createPatientP1(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - MethodOutcome resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); - assertTrue(resp.getCreated()); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); - assertNull(resp.getCreated()); - - ourLog.info("lastRequest: {}", ((GenericClient) client).getLastRequest()); - ourLog.info("lastResponse: {}", ((GenericClient) client).getLastResponse()); - ourLog.info("lastResponseBody: {}", ((GenericClient) client).getLastResponseBody()); - } - - @Test - public void testCreateWithStringAutoDetectsEncoding() throws Exception { - - Patient p1 = createPatientP1(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - int count = 0; - client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("value=\"John\"")); - count++; - - String resourceAsString = ourCtx.newJsonParser().encodeResourceToString(p1); - client - .create() - .resource(resourceAsString) - .execute(); - - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("[\"John\"]")); - count++; - - /* - * e.g. Now try with reversed encoding (provide a string that's in JSON and ask the client to use XML) - */ - - client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).encodedJson().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("[\"John\"]")); - count++; - - client.create().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).encodedXml().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("value=\"John\"")); - count++; - - } - - @Test - public void testCreateWithTag() throws Exception { - - Patient p1 = createPatientP1(); - p1.getMeta().addTag("http://hl7.org/fhir/tag", "urn:happytag", "This is a happy resource"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - MethodOutcome outcome = client.create().resource(p1).execute(); - assertEquals("44", outcome.getId().getIdPart()); - assertEquals("22", outcome.getId().getVersionIdPart()); - - int count = 0; - - assertEquals("http://example.com/fhir/Patient", capt.getValue().getURI().toString()); - assertEquals("POST", capt.getValue().getMethod()); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - count++; - - /* - * Try fluent options - */ - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - client.create().resource(p1).execute(); - assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(1).getURI().toString()); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - count++; - - String resourceText = "<Patient xmlns=\"http://hl7.org/fhir\"> </Patient>"; - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - client.create().resource(resourceText).execute(); - assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(2).getURI().toString()); - assertEquals(resourceText, IOUtils.toString(((HttpPost) capt.getAllValues().get(2)).getEntity().getContent())); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - count++; - } - - @Test - public void testCreateWithTagNonFluent() throws Exception { - - Patient p1 = createPatientP1(); - p1.getMeta().addTag("http://hl7.org/fhir/tag", "urn:happytag", "This is a happy resource"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - MethodOutcome outcome = client.create().resource(p1).execute(); - assertEquals("44", outcome.getId().getIdPart()); - assertEquals("22", outcome.getId().getVersionIdPart()); - - assertEquals("http://example.com/fhir/Patient", capt.getValue().getURI().toString()); - assertEquals("POST", capt.getValue().getMethod()); - Header catH = capt.getValue().getFirstHeader("Category"); - assertNull(catH); - } - - /** - * Test for issue #60 - */ - @Test - public void testCreateWithUtf8Characters() throws Exception { - String name = "測試醫院"; - Organization org = new Organization(); - org.setName(name); - org.addIdentifier().setSystem("urn:system").setValue("testCreateWithUtf8Characters_01"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - int count = 0; - client.create().resource(org).prettyPrint().encodedXml().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("<name value=\"測試醫院\"/>")); - count++; - - } - - @Test - public void testDelete() throws Exception { - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().addLocation("testDelete01"); - String ooStr = ourCtx.newXmlParser().encodeResourceToString(oo); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ooStr), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - OperationOutcome outcome = (OperationOutcome) client.delete().resourceById("Patient", "123").execute(); - - assertEquals("http://example.com/fhir/Patient/123", capt.getValue().getURI().toString()); - assertEquals("DELETE", capt.getValue().getMethod()); - assertEquals("testDelete01", outcome.getIssueFirstRep().getLocation().get(0).getValue()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("LKJHLKJGLKJKLL"), Charset.forName("UTF-8"))); - outcome = (OperationOutcome) client.delete().resourceById(new IdType("Location", "123", "456")).prettyPrint().encodedJson().execute(); - - assertEquals("http://example.com/fhir/Location/123?_pretty=true", capt.getAllValues().get(1).getURI().toString()); - assertEquals("DELETE", capt.getValue().getMethod()); - assertEquals(null, outcome); - - } - - @Test - public void testHistory() throws Exception { - - final String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { - @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; - Bundle response; - - response = client - .history() - .onServer() - .andReturnBundle(Bundle.class) - .execute(); - assertEquals("http://example.com/fhir/_history", capt.getAllValues().get(idx).getURI().toString()); - assertEquals(1, response.getEntry().size()); - idx++; - - response = client - .history() - .onType(Patient.class) - .andReturnBundle(Bundle.class) - .execute(); - assertEquals("http://example.com/fhir/Patient/_history", capt.getAllValues().get(idx).getURI().toString()); - assertEquals(1, response.getEntry().size()); - idx++; - - response = client - .history() - .onInstance(new IdType("Patient", "123")) - .andReturnBundle(Bundle.class) - .execute(); - assertEquals("http://example.com/fhir/Patient/123/_history", capt.getAllValues().get(idx).getURI().toString()); - assertEquals(1, response.getEntry().size()); - idx++; - } - - @Test - @Ignore - public void testInvalidCalls() { - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - try { - client.meta(); - fail(); - } catch (IllegalStateException e) { - assertEquals("Can not call $meta operations on a DSTU1 client", e.getMessage()); - } - try { - client.operation(); - fail(); - } catch (IllegalStateException e) { - assertEquals("Operations are only supported in FHIR DSTU2 and later. This client was created using a context configured for DSTU1", e.getMessage()); - } - } - - @Test - public void testLoadPageAndReturnDstu1Bundle() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - - 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); - client - .loadPage() - .byUrl("http://example.com/page1") - .andReturnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/page1", capt.getValue().getURI().toString()); - } - - @Test - public void testMissing() throws Exception { - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { - @Override - public InputStream answer(InvocationOnMock theInvocation) throws Throwable { - return (new ReaderInputStream(new StringReader(getPatientFeedWithOneResult()), Charset.forName("UTF-8"))); - } - }); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - - client.search().forResource("Patient").where(Patient.NAME.isMissing(true)).returnBundle(Bundle.class).execute(); - assertEquals("http://example.com/fhir/Patient?name%3Amissing=true", capt.getValue().getRequestLine().getUri()); - - client.search().forResource("Patient").where(Patient.NAME.isMissing(false)).returnBundle(Bundle.class).execute(); - assertEquals("http://example.com/fhir/Patient?name%3Amissing=false", capt.getValue().getRequestLine().getUri()); - } - - @Test - public void testRead() throws Exception { - - String msg = getResourceResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - Header[] headers = new Header[] { - new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), - new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - }; - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Patient response = client - .read() - .resource(Patient.class) - .withId(new IdType("Patient/1234")) - .execute(); - - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - - assertEquals("http://foo.com/Patient/123/_history/2333", response.getIdElement().getValue()); - - InstantType lm = response.getMeta().getLastUpdatedElement(); - lm.setTimeZoneZulu(true); - assertEquals("1995-11-15T04:58:08.000Z", lm.getValueAsString()); - - } - - @Test - public void testReadFluent() throws Exception { - - String msg = getResourceResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - Header[] headers = new Header[] { - new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), - new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - }; - when(myHttpResponse.getAllHeaders()).thenReturn(headers); + Patient p1 = createPatientP1(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + MethodOutcome resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); + assertTrue(resp.getCreated()); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + resp = client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); + assertNull(resp.getCreated()); + + ourLog.info("lastRequest: {}", ((GenericClient) client).getLastRequest()); + ourLog.info("lastResponse: {}", ((GenericClient) client).getLastResponse()); + ourLog.info("lastResponseBody: {}", ((GenericClient) client).getLastResponseBody()); + } + + @Test + public void testCreateWithStringAutoDetectsEncoding() throws Exception { + + Patient p1 = createPatientP1(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("value=\"John\"")); + count++; + + String resourceAsString = ourCtx.newJsonParser().encodeResourceToString(p1); + client + .create() + .resource(resourceAsString) + .execute(); + + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("[\"John\"]")); + count++; + + /* + * e.g. Now try with reversed encoding (provide a string that's in JSON and ask the client to use XML) + */ + + client.create().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).encodedJson().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("[\"John\"]")); + count++; + + client.create().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).encodedXml().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("value=\"John\"")); + count++; + + } + + @Test + public void testCreateWithTag() throws Exception { + + Patient p1 = createPatientP1(); + p1.getMeta().addTag("http://hl7.org/fhir/tag", "urn:happytag", "This is a happy resource"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + MethodOutcome outcome = client.create().resource(p1).execute(); + assertEquals("44", outcome.getId().getIdPart()); + assertEquals("22", outcome.getId().getVersionIdPart()); + + int count = 0; + + assertEquals("http://example.com/fhir/Patient", capt.getValue().getURI().toString()); + assertEquals("POST", capt.getValue().getMethod()); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + count++; + + /* + * Try fluent options + */ + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.create().resource(p1).execute(); + assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(1).getURI().toString()); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + count++; + + String resourceText = "<Patient xmlns=\"http://hl7.org/fhir\"> </Patient>"; + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.create().resource(resourceText).execute(); + assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(2).getURI().toString()); + assertEquals(resourceText, IOUtils.toString(((HttpPost) capt.getAllValues().get(2)).getEntity().getContent())); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + count++; + } + + @Test + public void testCreateWithTagNonFluent() throws Exception { + + Patient p1 = createPatientP1(); + p1.getMeta().addTag("http://hl7.org/fhir/tag", "urn:happytag", "This is a happy resource"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + MethodOutcome outcome = client.create().resource(p1).execute(); + assertEquals("44", outcome.getId().getIdPart()); + assertEquals("22", outcome.getId().getVersionIdPart()); + + assertEquals("http://example.com/fhir/Patient", capt.getValue().getURI().toString()); + assertEquals("POST", capt.getValue().getMethod()); + Header catH = capt.getValue().getFirstHeader("Category"); + assertNull(catH); + } + + /** + * Test for issue #60 + */ + @Test + public void testCreateWithUtf8Characters() throws Exception { + String name = "測試醫院"; + Organization org = new Organization(); + org.setName(name); + org.addIdentifier().setSystem("urn:system").setValue("testCreateWithUtf8Characters_01"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + client.create().resource(org).prettyPrint().encodedXml().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("<name value=\"測試醫院\"/>")); + count++; + + } + + @Test + public void testDelete() throws Exception { + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().addLocation("testDelete01"); + String ooStr = ourCtx.newXmlParser().encodeResourceToString(oo); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ooStr), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + OperationOutcome outcome = (OperationOutcome) client.delete().resourceById("Patient", "123").execute(); + + assertEquals("http://example.com/fhir/Patient/123", capt.getValue().getURI().toString()); + assertEquals("DELETE", capt.getValue().getMethod()); + assertEquals("testDelete01", outcome.getIssueFirstRep().getLocation().get(0).getValue()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("LKJHLKJGLKJKLL"), Charset.forName("UTF-8"))); + outcome = (OperationOutcome) client.delete().resourceById(new IdType("Location", "123", "456")).prettyPrint().encodedJson().execute(); + + assertEquals("http://example.com/fhir/Location/123?_pretty=true", capt.getAllValues().get(1).getURI().toString()); + assertEquals("DELETE", capt.getValue().getMethod()); + assertEquals(null, outcome); + + } + + @Test + public void testHistory() throws Exception { + + final String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { + @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; + Bundle response; + + response = client + .history() + .onServer() + .andReturnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/_history", capt.getAllValues().get(idx).getURI().toString()); + assertEquals(1, response.getEntry().size()); + idx++; + + response = client + .history() + .onType(Patient.class) + .andReturnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient/_history", capt.getAllValues().get(idx).getURI().toString()); + assertEquals(1, response.getEntry().size()); + idx++; + + response = client + .history() + .onInstance(new IdType("Patient", "123")) + .andReturnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient/123/_history", capt.getAllValues().get(idx).getURI().toString()); + assertEquals(1, response.getEntry().size()); + idx++; + } + + @Test + @Ignore + public void testInvalidCalls() { + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + try { + client.meta(); + fail(); + } catch (IllegalStateException e) { + assertEquals("Can not call $meta operations on a DSTU1 client", e.getMessage()); + } + try { + client.operation(); + fail(); + } catch (IllegalStateException e) { + assertEquals("Operations are only supported in FHIR DSTU2 and later. This client was created using a context configured for DSTU1", e.getMessage()); + } + } + + @Test + public void testLoadPageAndReturnDstu1Bundle() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + + 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); + client + .loadPage() + .byUrl("http://example.com/page1") + .andReturnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/page1", capt.getValue().getURI().toString()); + } + + @Test + public void testMissing() throws Exception { + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return (new ReaderInputStream(new StringReader(getPatientFeedWithOneResult()), Charset.forName("UTF-8"))); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + + client.search().forResource("Patient").where(Patient.NAME.isMissing(true)).returnBundle(Bundle.class).execute(); + assertEquals("http://example.com/fhir/Patient?name%3Amissing=true", capt.getValue().getRequestLine().getUri()); + + client.search().forResource("Patient").where(Patient.NAME.isMissing(false)).returnBundle(Bundle.class).execute(); + assertEquals("http://example.com/fhir/Patient?name%3Amissing=false", capt.getValue().getRequestLine().getUri()); + } + + @Test + public void testProcessMessage() throws IOException { + Bundle respBundle = new Bundle(); + respBundle.setType(BundleType.MESSAGE); + String respString = ourCtx.newJsonParser().encodeResourceToString(respBundle); + + ArgumentCaptor<HttpUriRequest> 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()).thenReturn(new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[0]); + + Bundle bundle = new Bundle(); + bundle.setType(BundleType.MESSAGE); + + Parameters parameters = new Parameters(); + parameters.addParameter() + .setName("content") + .setResource(bundle); + + int count = 0; + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + client.operation().onType(MessageHeader.class).named("$process-message").withParameters(parameters).execute(); + + assertEquals("http://example.com/fhir/MessageHeader/$process-message", capt.getAllValues().get(count).getURI().toString()); + String requestContent = IOUtils.toString(((HttpPost) capt.getAllValues().get(count)).getEntity().getContent(), Charsets.UTF_8); + assertThat(requestContent, startsWith("<Parameters xmlns=\"http://hl7.org/fhir\">")); + count++; + } + + @Test + public void testRead() throws Exception { + + String msg = getResourceResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + Header[] headers = new Header[] { + new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient response = client + .read() + .resource(Patient.class) + .withId(new IdType("Patient/1234")) + .execute(); + + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + + assertEquals("http://foo.com/Patient/123/_history/2333", response.getIdElement().getValue()); + + InstantType lm = response.getMeta().getLastUpdatedElement(); + lm.setTimeZoneZulu(true); + assertEquals("1995-11-15T04:58:08.000Z", lm.getValueAsString()); + + } + + @Test + public void testReadFluent() throws Exception { + + String msg = getResourceResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + Header[] headers = new Header[] { + new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + + Patient response = client.read().resource(Patient.class).withId(new IdType("Patient/1234")).execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = (Patient) client.read().resource("Patient").withId("1234").execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = (Patient) client.read().resource("Patient").withId(567L).execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/567", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.read().resource(Patient.class).withIdAndVersion("1234", "22").execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://example.com/fhir/Patient/1234/_history/22", capt.getAllValues().get(count++).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.read().resource(Patient.class).withUrl("http://foo/Patient/22").execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://foo/Patient/22", capt.getAllValues().get(count++).getURI().toString()); - int count = 0; + } - Patient response = client.read().resource(Patient.class).withId(new IdType("Patient/1234")).execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); + @Test + public void testReadWithAbsoluteUrl() throws Exception { - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = (Patient) client.read().resource("Patient").withId("1234").execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://example.com/fhir/Patient/1234", capt.getAllValues().get(count++).getURI().toString()); + String msg = getResourceResult(); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = (Patient) client.read().resource("Patient").withId(567L).execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://example.com/fhir/Patient/567", capt.getAllValues().get(count++).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.read().resource(Patient.class).withIdAndVersion("1234", "22").execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://example.com/fhir/Patient/1234/_history/22", capt.getAllValues().get(count++).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.read().resource(Patient.class).withUrl("http://foo/Patient/22").execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://foo/Patient/22", capt.getAllValues().get(count++).getURI().toString()); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + Header[] headers = new Header[] {new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - @Test - public void testReadWithAbsoluteUrl() throws Exception { + Patient response = client + .read() + .resource(Patient.class) + .withUrl(new IdType("http://somebase.com/path/to/base/Patient/1234")) + .execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://somebase.com/path/to/base/Patient/1234", capt.getAllValues().get(0).getURI().toString()); - String msg = getResourceResult(); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client + .read() + .resource(Patient.class) + .withUrl(new IdType("http://somebase.com/path/to/base/Patient/1234/_history/222")) + .execute(); + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://somebase.com/path/to/base/Patient/1234/_history/222", capt.getAllValues().get(1).getURI().toString()); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - Header[] headers = new Header[] { new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), - new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - }; - when(myHttpResponse.getAllHeaders()).thenReturn(headers); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + @SuppressWarnings("unused") + @Test + public void testSearchAllResources() throws Exception { - Patient response = client - .read() - .resource(Patient.class) - .withUrl(new IdType("http://somebase.com/path/to/base/Patient/1234")) - .execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://somebase.com/path/to/base/Patient/1234", capt.getAllValues().get(0).getURI().toString()); + String msg = getPatientFeedWithOneResult(); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client - .read() - .resource(Patient.class) - .withUrl(new IdType("http://somebase.com/path/to/base/Patient/1234/_history/222")) - .execute(); - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://somebase.com/path/to/base/Patient/1234/_history/222", capt.getAllValues().get(1).getURI().toString()); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - @SuppressWarnings("unused") - @Test - public void testSearchAllResources() throws Exception { + Bundle response = client.search() + .forAllResources() + .where(Patient.NAME.matches().value("james")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/?name=james", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchAutomaticallyUsesPost() throws Exception { - String msg = getPatientFeedWithOneResult(); + String msg = getPatientFeedWithOneResult(); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + String longValue = StringUtils.leftPad("", 20000, 'B'); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value(longValue)) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient/_search", capt.getValue().getURI().toString()); + + HttpEntityEnclosingRequestBase enc = (HttpEntityEnclosingRequestBase) capt.getValue(); + UrlEncodedFormEntity ent = (UrlEncodedFormEntity) enc.getEntity(); + String string = IOUtils.toString(ent.getContent()); + ourLog.info(string); + assertEquals("name=" + longValue, string); + } + + @Test + public void testSearchByCompartment() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + + 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); + Bundle response = client + .search() + .forResource(Patient.class) + .withIdAndCompartment("123", "fooCompartment") + .where(Patient.BIRTHDATE.afterOrEquals().day("2011-01-02")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://foo/Patient/123/fooCompartment?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); + + ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(response)); + + assertEquals("PRP1660", BundleUtil.toListOfResourcesOfType(ourCtx, response, Patient.class).get(0).getIdentifier().get(0).getValue()); + + try { + client + .search() + .forResource(Patient.class) + .withIdAndCompartment("", "fooCompartment") + .where(Patient.BIRTHDATE.afterOrEquals().day("2011-01-02")) + .returnBundle(Bundle.class) + .execute(); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.toString(), containsString("null or empty for compartment")); + } + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByComposite() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); + + Bundle response = client.search() + .forResource("Observation") + .where(Observation.CODE_VALUE_DATE + .withLeft(Observation.CODE.exactly().code("FOO$BAR")) + .withRight(Observation.VALUE_DATE.exactly().day("2001-01-01"))) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://foo/Observation?" + Observation.SP_CODE_VALUE_DATE + "=" + UrlUtil.escapeUrlParam("FOO\\$BAR$2001-01-01"), capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByDate() throws Exception { + + final String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { + @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; + + @SuppressWarnings("deprecation") + Bundle response = client.search() + .forResource(Patient.class) + .encodedJson() + .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22")) + .and(Patient.BIRTHDATE.after().day("2011-01-01")) + .include(Patient.INCLUDE_ORGANIZATION) + .sort().ascending(Patient.BIRTHDATE) + .sort().descending(Patient.NAME) + .sort().defaultOrder(Patient.ADDRESS) + .count(123) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort=birthdate%2C-name%2Caddress&_count=123&_format=json", + capt.getAllValues().get(idx++).getURI().toString()); + + response = client.search() + .forResource(Patient.class) + .encodedJson() + .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22")) + .and(Patient.BIRTHDATE.after().day("2011-01-01")) + .include(Patient.INCLUDE_ORGANIZATION) + .sort().ascending(Patient.BIRTHDATE) + .sort().descending(Patient.NAME) + .sort().defaultOrder(Patient.ADDRESS) + .count(123) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort=birthdate%2C-name%2Caddress&_count=123&_format=json", + capt.getAllValues().get(idx++).getURI().toString()); + + response = client.search() + .forResource(Patient.class) + .encodedJson() + .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22").orAfter().day("2020-01-01")) + .and(Patient.BIRTHDATE.after().day("2011-01-01")) + .returnBundle(Bundle.class) + .execute(); + + String comma = "%2C"; + assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22" + comma + "gt2020-01-01&birthdate=gt2011-01-01&_format=json", capt.getAllValues().get(idx++).getURI().toString()); + } + + @SuppressWarnings("unused") + @Test + public void testSearchByNumberExact() throws Exception { + + String msg = ourCtx.newXmlParser().encodeResourceToString(new Bundle()); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Observation.class) + .where(Observation.VALUE_QUANTITY.greaterThan().number(123).andUnits("foo", "bar")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Observation?value-quantity=gt123%7Cfoo%7Cbar", capt.getValue().getURI().toString()); + } + + @SuppressWarnings("unused") + @Test + public void testSearchByProfile() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .withProfile("http://1") + .withProfile("http://2") + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?_profile=http%3A%2F%2F1&_profile=http%3A%2F%2F2", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByQuantity() throws Exception { - Bundle response = client.search() - .forAllResources() - .where(Patient.NAME.matches().value("james")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/?name=james", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchAutomaticallyUsesPost() throws Exception { + String msg = getPatientFeedWithOneResult(); - String msg = getPatientFeedWithOneResult(); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - String longValue = StringUtils.leftPad("", 20000, 'B'); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().value(longValue)) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient/_search", capt.getValue().getURI().toString()); - - HttpEntityEnclosingRequestBase enc = (HttpEntityEnclosingRequestBase) capt.getValue(); - UrlEncodedFormEntity ent = (UrlEncodedFormEntity) enc.getEntity(); - String string = IOUtils.toString(ent.getContent()); - ourLog.info(string); - assertEquals("name=" + longValue, string); - } - - @Test - public void testSearchByCompartment() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - - 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); - Bundle response = client - .search() - .forResource(Patient.class) - .withIdAndCompartment("123", "fooCompartment") - .where(Patient.BIRTHDATE.afterOrEquals().day("2011-01-02")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://foo/Patient/123/fooCompartment?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); - - ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(response)); - - assertEquals("PRP1660", BundleUtil.toListOfResourcesOfType(ourCtx, response, Patient.class).get(0).getIdentifier().get(0).getValue()); - - try { - client - .search() - .forResource(Patient.class) - .withIdAndCompartment("", "fooCompartment") - .where(Patient.BIRTHDATE.afterOrEquals().day("2011-01-02")) - .returnBundle(Bundle.class) - .execute(); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.toString(), containsString("null or empty for compartment")); - } - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByComposite() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); - - Bundle response = client.search() - .forResource("Observation") - .where(Observation.CODE_VALUE_DATE - .withLeft(Observation.CODE.exactly().code("FOO$BAR")) - .withRight(Observation.VALUE_DATE.exactly().day("2001-01-01"))) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://foo/Observation?" + Observation.SP_CODE_VALUE_DATE + "=" + UrlUtil.escapeUrlParam("FOO\\$BAR$2001-01-01"), capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByDate() throws Exception { - - final String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { - @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; - - @SuppressWarnings("deprecation") - Bundle response = client.search() - .forResource(Patient.class) - .encodedJson() - .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22")) - .and(Patient.BIRTHDATE.after().day("2011-01-01")) - .include(Patient.INCLUDE_ORGANIZATION) - .sort().ascending(Patient.BIRTHDATE) - .sort().descending(Patient.NAME) - .sort().defaultOrder(Patient.ADDRESS) - .count(123) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort=birthdate%2C-name%2Caddress&_count=123&_format=json", - capt.getAllValues().get(idx++).getURI().toString()); - - response = client.search() - .forResource(Patient.class) - .encodedJson() - .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22")) - .and(Patient.BIRTHDATE.after().day("2011-01-01")) - .include(Patient.INCLUDE_ORGANIZATION) - .sort().ascending(Patient.BIRTHDATE) - .sort().descending(Patient.NAME) - .sort().defaultOrder(Patient.ADDRESS) - .count(123) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort=birthdate%2C-name%2Caddress&_count=123&_format=json", - capt.getAllValues().get(idx++).getURI().toString()); - - response = client.search() - .forResource(Patient.class) - .encodedJson() - .where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22").orAfter().day("2020-01-01")) - .and(Patient.BIRTHDATE.after().day("2011-01-01")) - .returnBundle(Bundle.class) - .execute(); - - String comma = "%2C"; - assertEquals("http://example.com/fhir/Patient?birthdate=le2012-01-22" + comma + "gt2020-01-01&birthdate=gt2011-01-01&_format=json", capt.getAllValues().get(idx++).getURI().toString()); - } - - @SuppressWarnings("unused") - @Test - public void testSearchByNumberExact() throws Exception { - - String msg = ourCtx.newXmlParser().encodeResourceToString(new Bundle()); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Observation.class) - .where(Observation.VALUE_QUANTITY.greaterThan().number(123).andUnits("foo", "bar")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Observation?value-quantity=gt123%7Cfoo%7Cbar", capt.getValue().getURI().toString()); - } - - @SuppressWarnings("unused") - @Test - public void testSearchByProfile() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .withProfile("http://1") - .withProfile("http://2") - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?_profile=http%3A%2F%2F1&_profile=http%3A%2F%2F2", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByQuantity() throws Exception { + Bundle response = client.search() + .forResource(Patient.class) + .where(Encounter.LENGTH.exactly().number(123).andNoUnits()) + .returnBundle(Bundle.class) + .execute(); - String msg = getPatientFeedWithOneResult(); + assertEquals("http://example.com/fhir/Patient?length=123%7C%7C", capt.getValue().getURI().toString()); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + @SuppressWarnings("unused") + @Test + public void testSearchByReferenceProperty() throws Exception { - Bundle response = client.search() - .forResource(Patient.class) - .where(Encounter.LENGTH.exactly().number(123).andNoUnits()) - .returnBundle(Bundle.class) - .execute(); + String msg = getPatientFeedWithOneResult(); - assertEquals("http://example.com/fhir/Patient?length=123%7C%7C", capt.getValue().getURI().toString()); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - @SuppressWarnings("unused") - @Test - public void testSearchByReferenceProperty() throws Exception { + Bundle response = client.search() + .forResource(Patient.class) + .where(Patient.GENERAL_PRACTITIONER.hasChainedProperty(Organization.NAME.matches().value("ORG0"))) + .returnBundle(Bundle.class) + .execute(); - String msg = getPatientFeedWithOneResult(); + assertEquals("http://example.com/fhir/Patient?general-practitioner.name=ORG0", capt.getValue().getURI().toString()); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + @SuppressWarnings("unused") + @Test + public void testSearchByReferenceSimple() throws Exception { - Bundle response = client.search() - .forResource(Patient.class) - .where(Patient.GENERAL_PRACTITIONER.hasChainedProperty(Organization.NAME.matches().value("ORG0"))) - .returnBundle(Bundle.class) - .execute(); + String msg = getPatientFeedWithOneResult(); - assertEquals("http://example.com/fhir/Patient?general-practitioner.name=ORG0", capt.getValue().getURI().toString()); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - @SuppressWarnings("unused") - @Test - public void testSearchByReferenceSimple() throws Exception { + Bundle response = client.search() + .forResource("Patient") + .where(Patient.GENERAL_PRACTITIONER.hasId("123")) + .returnBundle(Bundle.class) + .execute(); - String msg = getPatientFeedWithOneResult(); + assertEquals("http://example.com/fhir/Patient?general-practitioner=123", capt.getValue().getURI().toString()); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + @SuppressWarnings("unused") + @Test + public void testSearchBySecurity() throws Exception { - Bundle response = client.search() - .forResource("Patient") - .where(Patient.GENERAL_PRACTITIONER.hasId("123")) - .returnBundle(Bundle.class) - .execute(); + String msg = getPatientFeedWithOneResult(); - assertEquals("http://example.com/fhir/Patient?general-practitioner=123", capt.getValue().getURI().toString()); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - @SuppressWarnings("unused") - @Test - public void testSearchBySecurity() throws Exception { + Bundle response = client.search() + .forResource(Patient.class) + .withSecurity("urn:foo", "123") + .withSecurity("urn:bar", "456") + .returnBundle(Bundle.class) + .execute(); - String msg = getPatientFeedWithOneResult(); + assertEquals("http://example.com/fhir/Patient?_security=urn%3Afoo%7C123&_security=urn%3Abar%7C456", capt.getValue().getURI().toString()); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + @SuppressWarnings("unused") + @Test + public void testSearchByString() throws Exception { - Bundle response = client.search() - .forResource(Patient.class) - .withSecurity("urn:foo", "123") - .withSecurity("urn:bar", "456") - .returnBundle(Bundle.class) - .execute(); + String msg = getPatientFeedWithOneResult(); - assertEquals("http://example.com/fhir/Patient?_security=urn%3Afoo%7C123&_security=urn%3Abar%7C456", capt.getValue().getURI().toString()); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - @SuppressWarnings("unused") - @Test - public void testSearchByString() throws Exception { + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value("james")) + .returnBundle(Bundle.class) + .execute(); - String msg = getPatientFeedWithOneResult(); + assertEquals("http://example.com/fhir/Patient?name=james", capt.getValue().getURI().toString()); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().values("AAA", "BBB", "C,C")) + .returnBundle(Bundle.class) + .execute(); - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + assertEquals("http://example.com/fhir/Patient?name=" + UrlUtil.escapeUrlParam("AAA,BBB,C\\,C"), capt.getAllValues().get(1).getURI().toString()); - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().value("james")) - .returnBundle(Bundle.class) - .execute(); + } - assertEquals("http://example.com/fhir/Patient?name=james", capt.getValue().getURI().toString()); + @SuppressWarnings("unused") + @Test + public void testSearchByStringExact() throws Exception { - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().values("AAA", "BBB", "C,C")) - .returnBundle(Bundle.class) - .execute(); + String msg = getPatientFeedWithOneResult(); - assertEquals("http://example.com/fhir/Patient?name=" + UrlUtil.escapeUrlParam("AAA,BBB,C\\,C"), capt.getAllValues().get(1).getURI().toString()); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - @SuppressWarnings("unused") - @Test - public void testSearchByStringExact() throws Exception { + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matchesExactly().value("james")) + .returnBundle(Bundle.class) + .execute(); - String msg = getPatientFeedWithOneResult(); + assertEquals("http://example.com/fhir/Patient?name%3Aexact=james", capt.getValue().getURI().toString()); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + @SuppressWarnings("unused") + @Test + public void testSearchByTag() throws Exception { - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matchesExactly().value("james")) - .returnBundle(Bundle.class) - .execute(); + String msg = getPatientFeedWithOneResult(); - assertEquals("http://example.com/fhir/Patient?name%3Aexact=james", capt.getValue().getURI().toString()); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .withTag("urn:foo", "123") + .withTag("urn:bar", "456") + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?_tag=urn%3Afoo%7C123&_tag=urn%3Abar%7C456", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchByToken() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().systemAndCode("http://example.com/fhir", "ZZZ")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?identifier=http%3A%2F%2Fexample.com%2Ffhir%7CZZZ", capt.getValue().getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().code("ZZZ")) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?identifier=ZZZ", capt.getAllValues().get(1).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().codings(new Coding("A", "B", "ZZZ"), new Coding("C", "D", "ZZZ"))) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient?identifier=" + UrlUtil.escapeUrlParam("A|B,C|D"), capt.getAllValues().get(2).getURI().toString()); + + } + + /** + * Test for #192 + */ + @SuppressWarnings("unused") + @Test + public void testSearchByTokenWithEscaping() throws Exception { + final String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); + int index = 0; + String wantPrefix = "http://foo/Patient?identifier="; + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().systemAndCode("1", "2")) + .returnBundle(Bundle.class) + .execute(); + String wantValue = "1|2"; + String url = capt.getAllValues().get(index).getURI().toString(); + assertThat(url, Matchers.startsWith(wantPrefix)); + assertEquals(wantValue, UrlUtil.unescape(url.substring(wantPrefix.length()))); + assertEquals(UrlUtil.escapeUrlParam(wantValue), url.substring(wantPrefix.length())); + index++; + + response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().systemAndCode("1,2", "3,4")) + .returnBundle(Bundle.class) + .execute(); + wantValue = "1\\,2|3\\,4"; + url = capt.getAllValues().get(index).getURI().toString(); + assertThat(url, Matchers.startsWith(wantPrefix)); + assertEquals(wantValue, UrlUtil.unescape(url.substring(wantPrefix.length()))); + assertEquals(UrlUtil.escapeUrlParam(wantValue), url.substring(wantPrefix.length())); + index++; + } + + @SuppressWarnings("unused") + @Test + public void testSearchByTokenWithSystemAndNoCode() throws Exception { + + final String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { + @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; + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.hasSystemWithAnyCode("urn:foo")) + .returnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString()); + + response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", null)) + .returnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString()); + + response = client.search() + .forResource("Patient") + .where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", "")) + .returnBundle(Bundle.class) + .execute(); + assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString()); + } + + @SuppressWarnings("unused") + @Test + public void testSearchIncludeRecursive() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource(Patient.class) + .include(Patient.INCLUDE_ORGANIZATION) + .include(Patient.INCLUDE_LINK.asRecursive()) + .include(Patient.INCLUDE_ALL.asNonRecursive()) + .returnBundle(Bundle.class) + .execute(); + + assertThat(capt.getValue().getURI().toString(), containsString("http://example.com/fhir/Patient?")); + assertThat(capt.getValue().getURI().toString(), containsString("_include=" + UrlUtil.escapeUrlParam(Patient.INCLUDE_ORGANIZATION.getValue()))); + assertThat(capt.getValue().getURI().toString(), containsString("_include%3Arecurse=" + UrlUtil.escapeUrlParam(Patient.INCLUDE_LINK.getValue()))); + assertThat(capt.getValue().getURI().toString(), containsString("_include=*")); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchUsingGetSearch() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value("james")) + .usingStyle(SearchStyleEnum.GET_WITH_SEARCH) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient/_search?name=james", capt.getValue().getURI().toString()); + } + + @SuppressWarnings("unused") + @Test + public void testSearchUsingPost() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value("james")) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + + assertEquals("http://example.com/fhir/Patient/_search", capt.getValue().getURI().toString()); + + HttpEntityEnclosingRequestBase enc = (HttpEntityEnclosingRequestBase) capt.getValue(); + UrlEncodedFormEntity ent = (UrlEncodedFormEntity) enc.getEntity(); + String string = IOUtils.toString(ent.getContent()); + ourLog.info(string); + assertEquals("name=james", string); + } + + @Test + public void testSearchWithAbsoluteUrl() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client + .search() + .byUrl("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort%3Aasc=birthdate&_sort%3Adesc=name&_count=123&_format=json") + .returnBundle(Bundle.class) + .execute(); + + assertEquals(1, response.getEntry().size()); + } + + @SuppressWarnings("unused") + @Test + public void testSearchWithClientEncodingAndPrettyPrintConfig() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + client.setPrettyPrint(true); + client.setEncoding(EncodingEnum.JSON); - } + Bundle response = client.search() + .forResource(Patient.class) + .returnBundle(Bundle.class) + .execute(); - @SuppressWarnings("unused") - @Test - public void testSearchByTag() throws Exception { + assertEquals("http://example.com/fhir/Patient?_format=json&_pretty=true", capt.getValue().getURI().toString()); + + } + + @SuppressWarnings("unused") + @Test + public void testSearchWithEscapedParameters() throws Exception { - String msg = getPatientFeedWithOneResult(); + String msg = getPatientFeedWithOneResult(); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .withTag("urn:foo", "123") - .withTag("urn:bar", "456") - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?_tag=urn%3Afoo%7C123&_tag=urn%3Abar%7C456", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchByToken() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().systemAndCode("http://example.com/fhir", "ZZZ")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?identifier=http%3A%2F%2Fexample.com%2Ffhir%7CZZZ", capt.getValue().getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().code("ZZZ")) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?identifier=ZZZ", capt.getAllValues().get(1).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().codings(new Coding("A", "B", "ZZZ"), new Coding("C", "D", "ZZZ"))) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient?identifier=" + UrlUtil.escapeUrlParam("A|B,C|D"), capt.getAllValues().get(2).getURI().toString()); - - } - - /** - * Test for #192 - */ - @SuppressWarnings("unused") - @Test - public void testSearchByTokenWithEscaping() throws Exception { - final String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { - @Override - public InputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); - } - }); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://foo"); - int index = 0; - String wantPrefix = "http://foo/Patient?identifier="; - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().systemAndCode("1", "2")) - .returnBundle(Bundle.class) - .execute(); - String wantValue = "1|2"; - String url = capt.getAllValues().get(index).getURI().toString(); - assertThat(url, Matchers.startsWith(wantPrefix)); - assertEquals(wantValue, UrlUtil.unescape(url.substring(wantPrefix.length()))); - assertEquals(UrlUtil.escapeUrlParam(wantValue), url.substring(wantPrefix.length())); - index++; - - response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().systemAndCode("1,2", "3,4")) - .returnBundle(Bundle.class) - .execute(); - wantValue = "1\\,2|3\\,4"; - url = capt.getAllValues().get(index).getURI().toString(); - assertThat(url, Matchers.startsWith(wantPrefix)); - assertEquals(wantValue, UrlUtil.unescape(url.substring(wantPrefix.length()))); - assertEquals(UrlUtil.escapeUrlParam(wantValue), url.substring(wantPrefix.length())); - index++; - } - - @SuppressWarnings("unused") - @Test - public void testSearchByTokenWithSystemAndNoCode() throws Exception { - - final String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { - @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; - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.hasSystemWithAnyCode("urn:foo")) - .returnBundle(Bundle.class) - .execute(); - assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString()); - - response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", null)) - .returnBundle(Bundle.class) - .execute(); - assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString()); - - response = client.search() - .forResource("Patient") - .where(Patient.IDENTIFIER.exactly().systemAndCode("urn:foo", "")) - .returnBundle(Bundle.class) - .execute(); - assertEquals("http://example.com/fhir/Patient?identifier=urn%3Afoo%7C", capt.getAllValues().get(idx++).getURI().toString()); - } - - @SuppressWarnings("unused") - @Test - public void testSearchIncludeRecursive() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource(Patient.class) - .include(Patient.INCLUDE_ORGANIZATION) - .include(Patient.INCLUDE_LINK.asRecursive()) - .include(Patient.INCLUDE_ALL.asNonRecursive()) - .returnBundle(Bundle.class) - .execute(); - - assertThat(capt.getValue().getURI().toString(), containsString("http://example.com/fhir/Patient?")); - assertThat(capt.getValue().getURI().toString(), containsString("_include=" + UrlUtil.escapeUrlParam(Patient.INCLUDE_ORGANIZATION.getValue()))); - assertThat(capt.getValue().getURI().toString(), containsString("_include%3Arecurse=" + UrlUtil.escapeUrlParam(Patient.INCLUDE_LINK.getValue()))); - assertThat(capt.getValue().getURI().toString(), containsString("_include=*")); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchUsingGetSearch() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().value("james")) - .usingStyle(SearchStyleEnum.GET_WITH_SEARCH) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient/_search?name=james", capt.getValue().getURI().toString()); - } - - @SuppressWarnings("unused") - @Test - public void testSearchUsingPost() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().value("james")) - .usingStyle(SearchStyleEnum.POST) - .returnBundle(Bundle.class) - .execute(); - - assertEquals("http://example.com/fhir/Patient/_search", capt.getValue().getURI().toString()); - - HttpEntityEnclosingRequestBase enc = (HttpEntityEnclosingRequestBase) capt.getValue(); - UrlEncodedFormEntity ent = (UrlEncodedFormEntity) enc.getEntity(); - String string = IOUtils.toString(ent.getContent()); - ourLog.info(string); - assertEquals("name=james", string); - } - - @Test - public void testSearchWithAbsoluteUrl() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client - .search() - .byUrl("http://example.com/fhir/Patient?birthdate=le2012-01-22&birthdate=gt2011-01-01&_include=Patient%3Aorganization&_sort%3Aasc=birthdate&_sort%3Adesc=name&_count=123&_format=json") - .returnBundle(Bundle.class) - .execute(); - - assertEquals(1, response.getEntry().size()); - } - - @SuppressWarnings("unused") - @Test - public void testSearchWithClientEncodingAndPrettyPrintConfig() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - client.setPrettyPrint(true); - client.setEncoding(EncodingEnum.JSON); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - Bundle response = client.search() - .forResource(Patient.class) - .returnBundle(Bundle.class) - .execute(); + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Bundle response = client.search() + .forResource("Patient") + .where(Patient.NAME.matches().values("NE,NE", "NE,NE")) + .where(Patient.NAME.matchesExactly().values("E$E")) + .where(Patient.NAME.matches().values("NE\\NE")) + .where(Patient.NAME.matchesExactly().values("E|E")) + .returnBundle(Bundle.class) + .execute(); - assertEquals("http://example.com/fhir/Patient?_format=json&_pretty=true", capt.getValue().getURI().toString()); - - } - - @SuppressWarnings("unused") - @Test - public void testSearchWithEscapedParameters() throws Exception { + assertThat(capt.getValue().getURI().toString(), containsString("%3A")); + assertEquals("http://example.com/fhir/Patient?name=NE\\,NE,NE\\,NE&name=NE\\\\NE&name:exact=E\\$E&name:exact=E\\|E", UrlUtil.unescape(capt.getValue().getURI().toString())); + } - String msg = getPatientFeedWithOneResult(); + @SuppressWarnings("unused") + @Test + public void testSearchWithInternalServerError() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, "INTERNAL ERRORS")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Server Issues!"), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + try { + client + .search() + .forResource(Patient.class) + .returnBundle(Bundle.class) + .execute(); + fail(); + } catch (InternalErrorException e) { + assertEquals(e.getMessage(), "HTTP 500 INTERNAL ERRORS: Server Issues!"); + assertEquals(e.getResponseBody(), "Server Issues!"); + } - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.search() - .forResource("Patient") - .where(Patient.NAME.matches().values("NE,NE", "NE,NE")) - .where(Patient.NAME.matchesExactly().values("E$E")) - .where(Patient.NAME.matches().values("NE\\NE")) - .where(Patient.NAME.matchesExactly().values("E|E")) - .returnBundle(Bundle.class) - .execute(); + @SuppressWarnings("unused") + @Test + public void testSearchWithNonFhirResponse() throws Exception { + + String msg = getPatientFeedWithOneResult(); - assertThat(capt.getValue().getURI().toString(), containsString("%3A")); - assertEquals("http://example.com/fhir/Patient?name=NE\\,NE,NE\\,NE&name=NE\\\\NE&name:exact=E\\$E&name:exact=E\\|E", UrlUtil.unescape(capt.getValue().getURI().toString())); - } + ArgumentCaptor<HttpUriRequest> 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_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Server Issues!"), Charset.forName("UTF-8"))); - @SuppressWarnings("unused") - @Test - public void testSearchWithInternalServerError() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, "INTERNAL ERRORS")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Server Issues!"), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - try { - client - .search() - .forResource(Patient.class) - .returnBundle(Bundle.class) - .execute(); - fail(); - } catch (InternalErrorException e) { - assertEquals(e.getMessage(), "HTTP 500 INTERNAL ERRORS: Server Issues!"); - assertEquals(e.getResponseBody(), "Server Issues!"); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - } + try { + client.search().forResource(Patient.class).returnBundle(Bundle.class).execute(); + fail(); + } catch (NonFhirResponseException e) { + assertThat(e.getMessage(), StringContains.containsString("Server Issues!")); + } - @SuppressWarnings("unused") - @Test - public void testSearchWithNonFhirResponse() throws Exception { - - String msg = getPatientFeedWithOneResult(); + } - ArgumentCaptor<HttpUriRequest> 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_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Server Issues!"), Charset.forName("UTF-8"))); + @SuppressWarnings("unused") + @Test + public void testSearchWithReverseInclude() throws Exception { + + String msg = getPatientFeedWithOneResult(); - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - try { - client.search().forResource(Patient.class).returnBundle(Bundle.class).execute(); - fail(); - } catch (NonFhirResponseException e) { - assertThat(e.getMessage(), StringContains.containsString("Server Issues!")); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - } + Bundle response = client.search() + .forResource(Patient.class) + .encodedJson() + .revInclude(Provenance.INCLUDE_TARGET) + .returnBundle(Bundle.class) + .execute(); - @SuppressWarnings("unused") - @Test - public void testSearchWithReverseInclude() throws Exception { - - String msg = getPatientFeedWithOneResult(); + assertEquals("http://example.com/fhir/Patient?_revinclude=Provenance%3Atarget&_format=json", capt.getValue().getURI().toString()); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + @Test + public void testSetDefaultEncoding() throws Exception { + + String msg = ourCtx.newJsonParser().encodeResourceToString(new Patient()); + + ArgumentCaptor<HttpUriRequest> 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + // Header[] headers = new Header[] { new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 + // GMT"), + // new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + // new BasicHeader(Constants.HEADER_CATEGORY, "http://foo/tagdefinition.html; scheme=\"http://hl7.org/fhir/tag\"; + // label=\"Some tag\"") }; + // when(myHttpResponse.getAllHeaders()).thenReturn(headers); - Bundle response = client.search() - .forResource(Patient.class) - .encodedJson() - .revInclude(Provenance.INCLUDE_TARGET) - .returnBundle(Bundle.class) - .execute(); + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - assertEquals("http://example.com/fhir/Patient?_revinclude=Provenance%3Atarget&_format=json", capt.getValue().getURI().toString()); + (client).setEncoding(EncodingEnum.JSON); + int count = 0; - } + client + .read() + .resource(Patient.class) + .withId(new IdType("Patient/1234")) + .execute(); + assertEquals("http://example.com/fhir/Patient/1234?_format=json", capt.getAllValues().get(count).getURI().toString()); + count++; - @Test - public void testSetDefaultEncoding() throws Exception { - - String msg = ourCtx.newJsonParser().encodeResourceToString(new Patient()); - - ArgumentCaptor<HttpUriRequest> 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - // Header[] headers = new Header[] { new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 - // GMT"), - // new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - // new BasicHeader(Constants.HEADER_CATEGORY, "http://foo/tagdefinition.html; scheme=\"http://hl7.org/fhir/tag\"; - // label=\"Some tag\"") }; - // when(myHttpResponse.getAllHeaders()).thenReturn(headers); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + @Test + public void testTransaction() throws Exception { + Bundle input = createTransactionBundleInput(); + Bundle output = createTransactionBundleOutput(); - (client).setEncoding(EncodingEnum.JSON); - int count = 0; + String msg = ourCtx.newJsonParser().encodeResourceToString(output); + + ArgumentCaptor<HttpUriRequest> 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - client - .read() - .resource(Patient.class) - .withId(new IdType("Patient/1234")) - .execute(); - assertEquals("http://example.com/fhir/Patient/1234?_format=json", capt.getAllValues().get(count).getURI().toString()); - count++; + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - } + Bundle response = client.transaction() + .withBundle(input) + .execute(); - @Test - public void testTransaction() throws Exception { - Bundle input = createTransactionBundleInput(); - Bundle output = createTransactionBundleOutput(); + assertEquals("http://example.com/fhir", capt.getValue().getURI().toString()); + assertEquals(input.getEntry().get(0).getResource().getId(), response.getEntry().get(0).getResource().getId()); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - String msg = ourCtx.newJsonParser().encodeResourceToString(output); - - ArgumentCaptor<HttpUriRequest> 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()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + } - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + @Test + public void testTransactionXml() throws Exception { + Bundle input = createTransactionBundleInput(); + Bundle output = createTransactionBundleOutput(); - Bundle response = client.transaction() - .withBundle(input) - .execute(); + String msg = ourCtx.newXmlParser().encodeResourceToString(output); - assertEquals("http://example.com/fhir", capt.getValue().getURI().toString()); - assertEquals(input.getEntry().get(0).getResource().getId(), response.getEntry().get(0).getResource().getId()); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - @Test - public void testTransactionXml() throws Exception { - Bundle input = createTransactionBundleInput(); - Bundle output = createTransactionBundleOutput(); + Bundle response = client.transaction() + .withBundle(input) + .execute(); - String msg = ourCtx.newXmlParser().encodeResourceToString(output); + assertEquals("http://example.com/fhir", capt.getValue().getURI().toString()); + assertEquals(input.getEntry().get(0).getResource().getId(), response.getEntry().get(0).getResource().getId()); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + + } + + @Test + public void testUpdate() throws Exception { + + Patient p1 = new Patient(); + p1.addIdentifier().setSystem("foo:bar").setValue("12345"); + p1.addName().setFamily("Smith").addGiven("John"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Bundle response = client.transaction() - .withBundle(input) - .execute(); - - assertEquals("http://example.com/fhir", capt.getValue().getURI().toString()); - assertEquals(input.getEntry().get(0).getResource().getId(), response.getEntry().get(0).getResource().getId()); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - - } - - @Test - public void testUpdate() throws Exception { - - Patient p1 = new Patient(); - p1.addIdentifier().setSystem("foo:bar").setValue("12345"); - p1.addName().setFamily("Smith").addGiven("John"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - try { - client.update().resource(p1).execute(); - fail(); - } catch (InvalidRequestException e) { - // should happen because no ID set - } - - assertEquals(0, capt.getAllValues().size()); - - p1.setId("44"); - client.update().resource(p1).execute(); - - int count = 0; - - assertEquals(1, capt.getAllValues().size()); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - count++; - - MethodOutcome outcome = client.update().resource(p1).execute(); - assertEquals("44", outcome.getId().getIdPart()); - assertEquals("22", outcome.getId().getVersionIdPart()); - - assertEquals(2, capt.getAllValues().size()); - - assertEquals("http://example.com/fhir/Patient/44", capt.getValue().getURI().toString()); - assertEquals("PUT", capt.getValue().getMethod()); - - /* - * Try fluent options - */ - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - client.update().resource(p1).withId("123").execute(); - assertEquals(3, capt.getAllValues().size()); - assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(2).getURI().toString()); - - String resourceText = "<Patient xmlns=\"http://hl7.org/fhir\"> </Patient>"; - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - client.update().resource(resourceText).withId("123").execute(); - assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(3).getURI().toString()); - assertEquals(resourceText, IOUtils.toString(((HttpPut) capt.getAllValues().get(3)).getEntity().getContent())); - assertEquals(4, capt.getAllValues().size()); - - } - - @Test - public void testUpdateWithStringAutoDetectsEncoding() throws Exception { - - Patient p1 = new Patient(); - p1.addIdentifier().setSystem("foo:bar").setValue("12345"); - p1.addName().setFamily("Smith").addGiven("John"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - int count = 0; - client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).withId("1").execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("value=\"John\"")); - count++; - - client.update().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).withId("1").execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("[\"John\"]")); - count++; - - /* - * e.g. Now try with reversed encoding (provide a string that's in JSON and ask the client to use XML) - */ - - client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).withId("1").encodedJson().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("[\"John\"]")); - count++; - - client.update().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).withId("1").encodedXml().execute(); - assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertThat(extractBody(capt, count), containsString("value=\"John\"")); - count++; - } - - @Test - public void testVReadWithAbsoluteUrl() throws Exception { - - String msg = getResourceResult(); - - ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - Header[] headers = new Header[] { - new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), - new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), - }; - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Patient response = client - .read() - .resource(Patient.class) - .withUrl("http://somebase.com/path/to/base/Patient/1234/_history/2222") - .execute(); - - assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); - assertEquals("http://somebase.com/path/to/base/Patient/1234/_history/2222", capt.getAllValues().get(0).getURI().toString()); - - } - - @Test - public void testValidateNonFluent() throws Exception { - - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().setDiagnostics("OOOK"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {}); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ourCtx.newXmlParser().encodeResourceToString(oo)), Charset.forName("UTF-8"))); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - Patient p1 = new Patient(); - p1.addIdentifier().setSystem("foo:bar").setValue("12345"); - p1.addName().setFamily("Smith").addGiven("John"); - - MethodOutcome resp = client.validate(p1); - assertEquals("http://example.com/fhir/Patient/$validate", capt.getValue().getURI().toString()); - oo = (OperationOutcome) resp.getOperationOutcome(); - assertEquals("OOOK", oo.getIssueFirstRep().getDiagnostics()); - - } - - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - - @BeforeClass - public static void beforeClass() { - ourCtx = FhirContext.forR4(); - } + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + try { + client.update().resource(p1).execute(); + fail(); + } catch (InvalidRequestException e) { + // should happen because no ID set + } + + assertEquals(0, capt.getAllValues().size()); + + p1.setId("44"); + client.update().resource(p1).execute(); + + int count = 0; + + assertEquals(1, capt.getAllValues().size()); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + count++; + + MethodOutcome outcome = client.update().resource(p1).execute(); + assertEquals("44", outcome.getId().getIdPart()); + assertEquals("22", outcome.getId().getVersionIdPart()); + + assertEquals(2, capt.getAllValues().size()); + + assertEquals("http://example.com/fhir/Patient/44", capt.getValue().getURI().toString()); + assertEquals("PUT", capt.getValue().getMethod()); + + /* + * Try fluent options + */ + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.update().resource(p1).withId("123").execute(); + assertEquals(3, capt.getAllValues().size()); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(2).getURI().toString()); + + String resourceText = "<Patient xmlns=\"http://hl7.org/fhir\"> </Patient>"; + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.update().resource(resourceText).withId("123").execute(); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(3).getURI().toString()); + assertEquals(resourceText, IOUtils.toString(((HttpPut) capt.getAllValues().get(3)).getEntity().getContent())); + assertEquals(4, capt.getAllValues().size()); + + } + + @Test + public void testUpdateWithStringAutoDetectsEncoding() throws Exception { + + Patient p1 = new Patient(); + p1.addIdentifier().setSystem("foo:bar").setValue("12345"); + p1.addName().setFamily("Smith").addGiven("John"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int count = 0; + client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).withId("1").execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("value=\"John\"")); + count++; + + client.update().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).withId("1").execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("[\"John\"]")); + count++; + + /* + * e.g. Now try with reversed encoding (provide a string that's in JSON and ask the client to use XML) + */ + + client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p1)).withId("1").encodedJson().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("[\"John\"]")); + count++; + + client.update().resource(ourCtx.newJsonParser().encodeResourceToString(p1)).withId("1").encodedXml().execute(); + assertEquals(1, capt.getAllValues().get(count).getHeaders(Constants.HEADER_CONTENT_TYPE).length); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(count).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertThat(extractBody(capt, count), containsString("value=\"John\"")); + count++; + } + + @Test + public void testVReadWithAbsoluteUrl() throws Exception { + + String msg = getResourceResult(); + + ArgumentCaptor<HttpUriRequest> 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_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + Header[] headers = new Header[] { + new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333"), + }; + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient response = client + .read() + .resource(Patient.class) + .withUrl("http://somebase.com/path/to/base/Patient/1234/_history/2222") + .execute(); + + assertThat(response.getNameFirstRep().getFamily(), StringContains.containsString("Cardinal")); + assertEquals("http://somebase.com/path/to/base/Patient/1234/_history/2222", capt.getAllValues().get(0).getURI().toString()); + + } + + @Test + public void testValidateNonFluent() throws Exception { + + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().setDiagnostics("OOOK"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] {}); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ourCtx.newXmlParser().encodeResourceToString(oo)), Charset.forName("UTF-8"))); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient p1 = new Patient(); + p1.addIdentifier().setSystem("foo:bar").setValue("12345"); + p1.addName().setFamily("Smith").addGiven("John"); + + MethodOutcome resp = client.validate(p1); + assertEquals("http://example.com/fhir/Patient/$validate", capt.getValue().getURI().toString()); + oo = (OperationOutcome) resp.getOperationOutcome(); + assertEquals("OOOK", oo.getIssueFirstRep().getDiagnostics()); + + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() { + ourCtx = FhirContext.forR4(); + } } 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 17febb9ea89..5665c8be695 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 @@ -18,6 +18,7 @@ import ca.uhn.fhir.rest.server.interceptor.auth.*; import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy; 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.HttpEntity; import org.apache.http.HttpResponse; @@ -49,6 +50,9 @@ import java.util.concurrent.TimeUnit; 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 { @@ -84,6 +88,16 @@ public class AuthorizationInterceptorR4Test { return retVal; } + private Resource createDiagnosticReport(Integer theId, String theSubjectId) { + DiagnosticReport retVal = new DiagnosticReport(); + if (theId != null) { + retVal.setId(new IdType("DiagnosticReport", (long) theId)); + } + retVal.getCode().setText("OBS"); + retVal.setSubject(new Reference(theSubjectId)); + return retVal; + } + private HttpEntity createFhirResourceEntity(IBaseResource theResource) { String out = ourCtx.newJsonParser().encodeResourceToString(theResource); return new StringEntity(out, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8")); @@ -1573,6 +1587,46 @@ public class AuthorizationInterceptorR4Test { assertFalse(ourHitMethod); } + + @Test + public void testOperationTypeLevelDifferentBodyType() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("RULE 1").operation().named("process-message").onType(MessageHeader.class).andThen() + .build(); + } + }); + + HttpPost httpPost; + HttpResponse status; + String response; + + Bundle input = new Bundle(); + input.setType(Bundle.BundleType.MESSAGE); + String inputString = ourCtx.newJsonParser().encodeResourceToString(input); + + // With body + ourHitMethod = false; + httpPost = new HttpPost("http://localhost:" + ourPort + "/MessageHeader/$process-message"); + httpPost.setEntity(new StringEntity(inputString, ContentType.create(Constants.CT_FHIR_JSON_NEW, Charsets.UTF_8))); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // With body + ourHitMethod = false; + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/MessageHeader/$process-message"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + } + @Test public void testOperationWithTester() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -1612,6 +1666,56 @@ public class AuthorizationInterceptorR4Test { assertEquals(false, ourHitMethod); } + @Test + public void testPatchAllowed() throws IOException { + Observation obs = new Observation(); + obs.setSubject(new Reference("Patient/999")); + + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow().patch().allRequests().andThen() + .build(); + } + }); + + String patchBody = "[\n" + + " { \"op\": \"replace\", \"path\": \"Observation/status\", \"value\": \"amended\" }\n" + + " ]"; + HttpPatch patch = new HttpPatch("http://localhost:" + ourPort + "/Observation/123"); + patch.setEntity(new StringEntity(patchBody, ContentType.create(Constants.CT_JSON_PATCH, Charsets.UTF_8))); + CloseableHttpResponse status = ourClient.execute(patch); + extractResponseAndClose(status); + assertEquals(204, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + } + + @Test + public void testPatchNotAllowed() throws IOException { + Observation obs = new Observation(); + obs.setSubject(new Reference("Patient/999")); + + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow().metadata().andThen() + .build(); + } + }); + + String patchBody = "[\n" + + " { \"op\": \"replace\", \"path\": \"Observation/status\", \"value\": \"amended\" }\n" + + " ]"; + HttpPatch patch = new HttpPatch("http://localhost:" + ourPort + "/Observation/123"); + patch.setEntity(new StringEntity(patchBody, ContentType.create(Constants.CT_JSON_PATCH, Charsets.UTF_8))); + CloseableHttpResponse status = ourClient.execute(patch); + extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + } + @Test public void testReadByAnyId() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -1806,6 +1910,83 @@ public class AuthorizationInterceptorR4Test { } + @Test + public void testReadByCompartmentReadByIdParam() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("Rule 1").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen() + .build(); + } + }); + + HttpGet httpGet; + HttpResponse status; + + ourReturn = Collections.singletonList(createPatient(1)); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/1"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourReturn = Collections.singletonList(createPatient(1)); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=1"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/2"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + } + + @Test + public void testReadByCompartmentReadByPatientParam() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("Rule 1").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen() + .build(); + } + }); + + HttpGet httpGet; + HttpResponse status; + + ourReturn = Collections.singletonList(createDiagnosticReport(1, "Patient/1")); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/DiagnosticReport?patient=Patient/1"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourReturn = Collections.singletonList(createDiagnosticReport(1, "Patient/1")); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/DiagnosticReport?patient=1"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourReturn = Collections.singletonList(createDiagnosticReport(1, "Patient/1")); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/DiagnosticReport?patient=Patient/2"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + } + @Test public void testReadByCompartmentRight() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -2767,6 +2948,7 @@ public class AuthorizationInterceptorR4Test { @Override public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { return new RuleBuilder() + .allow("Rule 1").patch().allRequests().andThen() .allow("Rule 1").write().instance("Patient/900").andThen() .build(); } @@ -2784,14 +2966,6 @@ public class AuthorizationInterceptorR4Test { extractResponseAndClose(status); assertEquals(204, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); - - ourHitMethod = false; - httpPost = new HttpPatch("http://localhost:" + ourPort + "/Patient/999"); - httpPost.setEntity(new StringEntity(input, ContentType.parse("application/json-patch+json"))); - status = ourClient.execute(httpPost); - extractResponseAndClose(status); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); } @AfterClass @@ -2811,12 +2985,14 @@ public class AuthorizationInterceptorR4Test { 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); + ourServlet.setResourceProviders(patProvider, obsProv, encProv, cpProv, orgProv, drProv, mshProv); ourServlet.setPlainProviders(plainProvider); ourServlet.setPagingProvider(new FifoMemoryPagingProvider(100)); ServletHolder servletHolder = new ServletHolder(ourServlet); @@ -2892,6 +3068,40 @@ public class AuthorizationInterceptorR4Test { } + public static class DummyMessageHeaderResourceProvider implements IResourceProvider { + + + @Override + public Class<? extends IBaseResource> getResourceType() { + return MessageHeader.class; + } + + @Operation(name = "process-message", idempotent = true) + public Parameters operation0(@OperationParam(name="content") Bundle theInput) { + ourHitMethod = true; + return (Parameters) new Parameters().setId("1"); + } + + } + + public static class DummyDiagnosticReportResourceProvider implements IResourceProvider { + + + @Override + public Class<? extends IBaseResource> getResourceType() { + return DiagnosticReport.class; + } + + @Search() + public List<Resource> search( + @OptionalParam(name = "subject") ReferenceParam theSubject, + @OptionalParam(name = "patient") ReferenceParam thePatient + ) { + ourHitMethod = true; + return ourReturn; + } + } + @SuppressWarnings("unused") public static class DummyObservationResourceProvider implements IResourceProvider { @@ -2934,6 +3144,12 @@ public class AuthorizationInterceptorR4Test { return (Parameters) new Parameters().setId("1"); } + @Patch + public MethodOutcome patch(@IdParam IdType theId, PatchTypeEnum thePatchType, @ResourceParam String theBody) { + ourHitMethod = true; + return new MethodOutcome().setId(theId.withVersion("2")); + } + @Read(version = true) public Observation read(@IdParam IdType theId) { ourHitMethod = true; @@ -3076,7 +3292,7 @@ public class AuthorizationInterceptorR4Test { } @Search() - public List<Resource> search() { + public List<Resource> search(@OptionalParam(name = "_id") IdType theIdParam) { ourHitMethod = true; return ourReturn; } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/InjectionAttackTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/InjectionAttackTest.java new file mode 100644 index 00000000000..4d0c63f3727 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/InjectionAttackTest.java @@ -0,0 +1,251 @@ +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.OptionalParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.param.TokenParam; +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 ca.uhn.fhir.util.UrlUtil; +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.r4.model.IdType; +import org.hl7.fhir.r4.model.Patient; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.*; + +public class InjectionAttackTest { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(InjectionAttackTest.class); + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx = FhirContext.forR4(); + private static int ourPort; + private static Server ourServer; + private static RestfulServer ourServlet; + + @Test + public void testPreventHtmlInjectionViaInvalidContentType() throws Exception { + String requestUrl = "http://localhost:" + + ourPort + + "/Patient/123"; + + // XML HTML + HttpGet httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, "application/<script>"); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(200, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + } + } + + @Test + public void testPreventHtmlInjectionViaInvalidParameterName() throws Exception { + String requestUrl = "http://localhost:" + + ourPort + + "/Patient?a" + + UrlUtil.escapeUrlParam("<script>") + + "=123"; + + // XML HTML + HttpGet httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML + ", " + Constants.CT_FHIR_XML_NEW); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(400, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + assertEquals("text/html", status.getFirstHeader("Content-Type").getValue().toLowerCase().replaceAll(";.*", "").trim()); + } + + // JSON HTML + httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML + ", " + Constants.CT_FHIR_JSON_NEW); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(400, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + assertEquals("text/html", status.getFirstHeader("Content-Type").getValue().toLowerCase().replaceAll(";.*", "").trim()); + } + + // XML HTML + httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_XML_NEW); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(400, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + assertEquals(Constants.CT_FHIR_XML_NEW, status.getFirstHeader("Content-Type").getValue().toLowerCase().replaceAll(";.*", "").trim()); + } + + // JSON Plain + httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON_NEW); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(400, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + assertEquals(Constants.CT_FHIR_JSON_NEW, status.getFirstHeader("Content-Type").getValue().toLowerCase().replaceAll(";.*", "").trim()); + } + } + + @Test + public void testPreventHtmlInjectionViaInvalidResourceType() throws Exception { + String requestUrl = "http://localhost:" + + ourPort + + "/AA" + + UrlUtil.escapeUrlParam("<script>"); + + // XML HTML + HttpGet httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML + ", " + Constants.CT_FHIR_XML_NEW); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(404, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + assertEquals("text/html", status.getFirstHeader("Content-Type").getValue().toLowerCase().replaceAll(";.*", "").trim()); + } + + // JSON HTML + httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML + ", " + Constants.CT_FHIR_JSON_NEW); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(404, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + assertEquals("text/html", status.getFirstHeader("Content-Type").getValue().toLowerCase().replaceAll(";.*", "").trim()); + } + + // XML HTML + httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_XML_NEW); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(404, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + assertEquals(Constants.CT_FHIR_XML_NEW, status.getFirstHeader("Content-Type").getValue().toLowerCase().replaceAll(";.*", "").trim()); + } + + // JSON Plain + httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON_NEW); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(404, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + assertEquals(Constants.CT_FHIR_JSON_NEW, status.getFirstHeader("Content-Type").getValue().toLowerCase().replaceAll(";.*", "").trim()); + } + } + + @Test + public void testPreventHtmlInjectionViaInvalidTokenParamModifier() throws Exception { + String requestUrl = "http://localhost:" + + ourPort + + "/Patient?identifier:" + + UrlUtil.escapeUrlParam("<script>") + + "=123"; + HttpGet httpGet = new HttpGet(requestUrl); + httpGet.addHeader(Constants.HEADER_ACCEPT, "application/<script>"); + try (CloseableHttpResponse status = ourClient.execute(httpGet)) { + String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(responseContent); + + assertEquals(200, status.getStatusLine().getStatusCode()); + assertThat(responseContent, not(containsString("<script>"))); + } + + } + + @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 patientProvider = new DummyPatientResourceProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + ourServlet = new RestfulServer(ourCtx); + ourServlet.setFhirContext(ourCtx); + ourServlet.setResourceProviders(patientProvider); + ourServlet.registerInterceptor(new ResponseHighlighterInterceptor()); + 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 DummyPatientResourceProvider implements IResourceProvider { + + @Override + public Class<? extends Patient> getResourceType() { + return Patient.class; + } + + @Read + public Patient read(@IdParam IdType theId) { + Patient patient = new Patient(); + patient.setId(theId); + patient.setActive(true); + return patient; + } + + @Search + public List<Patient> search(@OptionalParam(name = "identifier") TokenParam theToken) { + return new ArrayList<>(); + } + + + } + +} diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerWithResponseHighlightingInterceptorExceptionTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServerWithResponseHighlightingInterceptorExceptionTest.java similarity index 86% rename from hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerWithResponseHighlightingInterceptorExceptionTest.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServerWithResponseHighlightingInterceptorExceptionTest.java index 72fb556250d..0a8f2f0dd5e 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerWithResponseHighlightingInterceptorExceptionTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServerWithResponseHighlightingInterceptorExceptionTest.java @@ -1,11 +1,16 @@ -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.util.concurrent.TimeUnit; +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.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.param.TokenParam; +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.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; @@ -15,29 +20,21 @@ 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.r4.model.IdType; +import org.hl7.fhir.r4.model.Patient; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.dstu2.resource.Patient; -import ca.uhn.fhir.model.primitive.IdDt; -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.param.TokenParam; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; -import ca.uhn.fhir.util.PortUtil; -import ca.uhn.fhir.util.TestUtil; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.*; public class ServerWithResponseHighlightingInterceptorExceptionTest { - private static CloseableHttpClient ourClient; - - private static FhirContext ourCtx = FhirContext.forDstu2(); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerWithResponseHighlightingInterceptorExceptionTest.class); + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx = FhirContext.forR4(); private static int ourPort; private static Server ourServer; private static RestfulServer ourServlet; @@ -49,7 +46,7 @@ public class ServerWithResponseHighlightingInterceptorExceptionTest { String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(responseContent); - + assertEquals(400, status.getStatusLine().getStatusCode()); assertThat(responseContent, containsString("<diagnostics value=\"AAABBB\"/>")); } @@ -62,12 +59,11 @@ public class ServerWithResponseHighlightingInterceptorExceptionTest { String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(responseContent); - + assertEquals(500, status.getStatusLine().getStatusCode()); assertThat(responseContent, containsString("<diagnostics value=\"Failed to call access method: java.lang.Error: AAABBB\"/>")); } - @AfterClass public static void afterClassClearContext() throws Exception { ourServer.stop(); @@ -102,20 +98,21 @@ public class ServerWithResponseHighlightingInterceptorExceptionTest { public static class DummyPatientResourceProvider implements IResourceProvider { @Override - public Class<? extends IResource> getResourceType() { + public Class<? extends Patient> getResourceType() { return Patient.class; } @Read - public Patient read(@IdParam IdDt theId) { + public Patient read(@IdParam IdType theId) { throw new InvalidRequestException("AAABBB"); } @Search - public Patient search(@RequiredParam(name="identifier") TokenParam theToken) { + public Patient search(@RequiredParam(name = "identifier") TokenParam theToken) { throw new Error("AAABBB"); } + } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/CachingValidationSupport.java new file mode 100644 index 00000000000..59c6ea38d42 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/CachingValidationSupport.java @@ -0,0 +1,67 @@ +package org.hl7.fhir.dstu3.hapi.validation; + +import ca.uhn.fhir.context.FhirContext; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; +import org.hl7.fhir.dstu3.model.CodeSystem; +import org.hl7.fhir.dstu3.model.StructureDefinition; +import org.hl7.fhir.dstu3.model.ValueSet; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("unchecked") +public class CachingValidationSupport implements IValidationSupport { + + private final IValidationSupport myWrap; + private final Cache<String, Object> myCache; + + public CachingValidationSupport(IValidationSupport theWrap) { + myWrap = theWrap; + myCache = Caffeine.newBuilder().expireAfterWrite(60, TimeUnit.SECONDS).build(); + } + + @Override + public ValueSet.ValueSetExpansionComponent expandValueSet(FhirContext theContext, ValueSet.ConceptSetComponent theInclude) { + return myWrap.expandValueSet(theContext, theInclude); + } + + @Override + public List<IBaseResource> fetchAllConformanceResources(FhirContext theContext) { + return (List<IBaseResource>) myCache.get("fetchAllConformanceResources", + t -> myWrap.fetchAllConformanceResources(theContext)); + } + + @Override + public List<StructureDefinition> fetchAllStructureDefinitions(FhirContext theContext) { + return (List<StructureDefinition>) myCache.get("fetchAllStructureDefinitions", + t -> myWrap.fetchAllStructureDefinitions(theContext)); + } + + @Override + public CodeSystem fetchCodeSystem(FhirContext theContext, String theSystem) { + return myWrap.fetchCodeSystem(theContext, theSystem); + } + + @Override + public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { + return myWrap.fetchResource(theContext, theClass, theUri); + } + + @Override + public StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl) { + return myWrap.fetchStructureDefinition(theCtx, theUrl); + } + + @Override + public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) { + return myWrap.isCodeSystemSupported(theContext, theSystem); + } + + @Override + public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { + return myWrap.validateCode(theContext, theCodeSystem, theCode, theDisplay); + } +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/hapi/validation/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/hapi/validation/CachingValidationSupport.java new file mode 100644 index 00000000000..3587b622d97 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/hapi/validation/CachingValidationSupport.java @@ -0,0 +1,56 @@ +package org.hl7.fhir.instance.hapi.validation; + +import ca.uhn.fhir.context.FhirContext; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.hl7.fhir.instance.model.StructureDefinition; +import org.hl7.fhir.instance.model.ValueSet; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("unchecked") +public class CachingValidationSupport implements IValidationSupport { + + private final IValidationSupport myWrap; + private final Cache<String, Object> myCache; + + public CachingValidationSupport(IValidationSupport theWrap) { + myWrap = theWrap; + myCache = Caffeine.newBuilder().expireAfterWrite(60, TimeUnit.SECONDS).build(); + } + + + @Override + public List<StructureDefinition> allStructures() { + return (List<StructureDefinition>) myCache.get("fetchAllStructureDefinitions", + t -> myWrap.allStructures()); + } + + @Override + public ValueSet.ValueSetExpansionComponent expandValueSet(FhirContext theContext, ValueSet.ConceptSetComponent theInclude) { + return myWrap.expandValueSet(theContext, theInclude); + } + + @Override + public ValueSet fetchCodeSystem(FhirContext theContext, String theSystem) { + return myWrap.fetchCodeSystem(theContext, theSystem); + } + + @Override + public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { + return myWrap.fetchResource(theContext, theClass, theUri); + } + + @Override + public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) { + return myWrap.isCodeSystemSupported(theContext, theSystem); + } + + @Override + public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { + return myWrap.validateCode(theContext, theCodeSystem, theCode, theDisplay); + } + +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/CachingValidationSupport.java new file mode 100644 index 00000000000..4663b0149da --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/CachingValidationSupport.java @@ -0,0 +1,67 @@ +package org.hl7.fhir.r4.hapi.validation; + +import ca.uhn.fhir.context.FhirContext; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("unchecked") +public class CachingValidationSupport implements IValidationSupport { + + private final IValidationSupport myWrap; + private final Cache<String, Object> myCache; + + public CachingValidationSupport(IValidationSupport theWrap) { + myWrap = theWrap; + myCache = Caffeine.newBuilder().expireAfterWrite(60, TimeUnit.SECONDS).build(); + } + + @Override + public ValueSet.ValueSetExpansionComponent expandValueSet(FhirContext theContext, ValueSet.ConceptSetComponent theInclude) { + return myWrap.expandValueSet(theContext, theInclude); + } + + @Override + public List<IBaseResource> fetchAllConformanceResources(FhirContext theContext) { + return (List<IBaseResource>) myCache.get("fetchAllConformanceResources", + t -> myWrap.fetchAllConformanceResources(theContext)); + } + + @Override + public List<StructureDefinition> fetchAllStructureDefinitions(FhirContext theContext) { + return (List<StructureDefinition>) myCache.get("fetchAllStructureDefinitions", + t -> myWrap.fetchAllStructureDefinitions(theContext)); + } + + @Override + public CodeSystem fetchCodeSystem(FhirContext theContext, String theSystem) { + return myWrap.fetchCodeSystem(theContext, theSystem); + } + + @Override + public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) { + return myWrap.fetchResource(theContext, theClass, theUri); + } + + @Override + public StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl) { + return myWrap.fetchStructureDefinition(theCtx, theUrl); + } + + @Override + public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) { + return myWrap.isCodeSystemSupported(theContext, theSystem); + } + + @Override + public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { + return myWrap.validateCode(theContext, theCodeSystem, theCode, theDisplay); + } +} diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/PrePopulatedValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/PrePopulatedValidationSupport.java similarity index 98% rename from hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/PrePopulatedValidationSupport.java rename to hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/PrePopulatedValidationSupport.java index 92807c4f6c3..71ff11ed74d 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/PrePopulatedValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/PrePopulatedValidationSupport.java @@ -1,8 +1,9 @@ -package org.hl7.fhir.r4.hapi.ctx; +package org.hl7.fhir.r4.hapi.validation; import ca.uhn.fhir.context.FhirContext; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.MetadataResource; import org.hl7.fhir.r4.model.StructureDefinition; diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/ValidationSupportChain.java similarity index 97% rename from hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/ValidationSupportChain.java rename to hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/ValidationSupportChain.java index 9d63c85bdb2..7fd2de1e42f 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/ctx/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/ValidationSupportChain.java @@ -1,7 +1,8 @@ -package org.hl7.fhir.r4.hapi.ctx; +package org.hl7.fhir.r4.hapi.validation; import ca.uhn.fhir.context.FhirContext; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; diff --git a/hapi-fhir-validation/src/test/java/fluentpath/Example20_ValidateResource.java b/hapi-fhir-validation/src/test/java/fluentpath/Example20_ValidateResource.java new file mode 100644 index 00000000000..b9c5c43576a --- /dev/null +++ b/hapi-fhir-validation/src/test/java/fluentpath/Example20_ValidateResource.java @@ -0,0 +1,30 @@ +package fluentpath; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ValidationResult; +import org.hl7.fhir.dstu3.model.Encounter; +import org.hl7.fhir.dstu3.model.OperationOutcome; + +public class Example20_ValidateResource { + public static void main(String[] args) { + + // Create an incomplete encounter (status is required) + Encounter enc = new Encounter(); + enc.addIdentifier().setSystem("http://acme.org/encNums").setValue("12345"); + + // Create a new validator + FhirContext ctx = FhirContext.forDstu3(); + FhirValidator validator = ctx.newValidator(); + + // Did we succeed? + ValidationResult result = validator.validateWithResult(enc); + System.out.println("Success: " + result.isSuccessful()); + + // What was the result + OperationOutcome outcome = (OperationOutcome) result.toOperationOutcome(); + IParser parser = ctx.newXmlParser().setPrettyPrint(true); + System.out.println(parser.encodeResourceToString(outcome)); + } +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index 75a548bc53b..937475905b6 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -77,7 +77,7 @@ public class FhirInstanceValidatorDstu3Test { myVal.setValidateAgainstStandardSchematron(false); myMockSupport = mock(IValidationSupport.class); - ValidationSupportChain validationSupport = new ValidationSupportChain(myMockSupport, myDefaultValidationSupport); + CachingValidationSupport validationSupport = new CachingValidationSupport(new ValidationSupportChain(myMockSupport, myDefaultValidationSupport)); myInstanceVal = new FhirInstanceValidator(validationSupport); myVal.registerValidatorModule(myInstanceVal); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/utils/FhirPathEngineTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/utils/FhirPathEngineTest.java index a03f994b4e3..659e68f3a8d 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/utils/FhirPathEngineTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/utils/FhirPathEngineTest.java @@ -1,45 +1,59 @@ package org.hl7.fhir.dstu3.utils; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.util.List; - -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext; +import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.exceptions.FHIRException; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.util.TestUtil; +import java.util.List; + +import static org.junit.Assert.*; public class FhirPathEngineTest { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirPathEngineTest.class); private static FhirContext ourCtx = FhirContext.forDstu3(); private static FHIRPathEngine ourEngine; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirPathEngineTest.class); @Test public void testAs() throws Exception { Observation obs = new Observation(); obs.setValue(new StringType("FOO")); - + List<Base> value = ourEngine.evaluate(obs, "Observation.value.as(String)"); assertEquals(1, value.size()); - assertEquals("FOO", ((StringType)value.get(0)).getValue()); + assertEquals("FOO", ((StringType) value.get(0)).getValue()); } - + + @Test + public void testCrossResourceBoundaries() throws FHIRException { + Specimen specimen = new Specimen(); + specimen.setId("#FOO"); + specimen.setReceivedTimeElement(new DateTimeType("2011-01-01")); + Observation o = new Observation(); + o.getContained().add(specimen); + + o.setId("O1"); + o.setStatus(Observation.ObservationStatus.FINAL); + o.setSpecimen(new Reference("#FOO")); + + List<Base> value = ourEngine.evaluate(o, "Observation.specimen.resolve().receivedTime"); + assertEquals(1, value.size()); + assertEquals("2011-01-01", ((DateTimeType) value.get(0)).getValueAsString()); + } + @Test public void testExistsWithNoValue() throws FHIRException { Patient patient = new Patient(); patient.setDeceased(new BooleanType()); List<Base> eval = ourEngine.evaluate(patient, "Patient.deceased.exists()"); ourLog.info(eval.toString()); - assertFalse(((BooleanType)eval.get(0)).getValue()); + assertFalse(((BooleanType) eval.get(0)).getValue()); } @Test @@ -48,7 +62,7 @@ public class FhirPathEngineTest { patient.setDeceased(new BooleanType(false)); List<Base> eval = ourEngine.evaluate(patient, "Patient.deceased.exists()"); ourLog.info(eval.toString()); - assertTrue(((BooleanType)eval.get(0)).getValue()); + assertTrue(((BooleanType) eval.get(0)).getValue()); } @AfterClass diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/utils/FhirPathEngineR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/utils/FhirPathEngineR4Test.java index 155633c3f4e..61e5045b745 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/utils/FhirPathEngineR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/utils/FhirPathEngineR4Test.java @@ -3,14 +3,12 @@ package org.hl7.fhir.r4.utils; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.dstu3.utils.FhirPathEngineTest; +import org.hl7.fhir.exceptions.FHIRException; 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.FHIRPathEngine; -import org.hl7.fhir.exceptions.FHIRException; import org.junit.AfterClass; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import java.util.List; @@ -23,6 +21,23 @@ public class FhirPathEngineR4Test { private static FHIRPathEngine ourEngine; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirPathEngineTest.class); + @Test + public void testCrossResourceBoundaries() throws FHIRException { + Specimen specimen = new Specimen(); + specimen.setId("#FOO"); + specimen.setReceivedTimeElement(new DateTimeType("2011-01-01")); + Observation o = new Observation(); + o.getContained().add(specimen); + + o.setId("O1"); + o.setStatus(Observation.ObservationStatus.FINAL); + o.setSpecimen(new Reference("#FOO")); + + List<Base> value = ourEngine.evaluate(o, "Observation.specimen.resolve().receivedTime"); + assertEquals(1, value.size()); + assertEquals("2011-01-01", ((DateTimeType) value.get(0)).getValueAsString()); + } + @Test public void testAs() throws Exception { Observation obs = new Observation(); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index 4c852810973..d7769928670 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -17,7 +17,10 @@ import org.hl7.fhir.r4.conformance.ProfileUtilities; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.hapi.ctx.*; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport.CodeValidationResult; +import org.hl7.fhir.r4.hapi.validation.CachingValidationSupport; import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator; +import org.hl7.fhir.r4.hapi.validation.PrePopulatedValidationSupport; +import org.hl7.fhir.r4.hapi.validation.ValidationSupportChain; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; @@ -94,7 +97,7 @@ public class FhirInstanceValidatorR4Test { myVal.setValidateAgainstStandardSchematron(false); myMockSupport = mock(IValidationSupport.class); - ValidationSupportChain validationSupport = new ValidationSupportChain(myMockSupport, myDefaultValidationSupport); + CachingValidationSupport validationSupport = new CachingValidationSupport(new ValidationSupportChain(myMockSupport, myDefaultValidationSupport)); myInstanceVal = new FhirInstanceValidator(validationSupport); myVal.registerValidatorModule(myInstanceVal); @@ -539,7 +542,7 @@ public class FhirInstanceValidatorR4Test { public void testValidateProfileWithExtension() throws IOException, FHIRException { PrePopulatedValidationSupport valSupport = new PrePopulatedValidationSupport(); DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(); - ValidationSupportChain support = new ValidationSupportChain(valSupport, defaultSupport); + CachingValidationSupport support = new CachingValidationSupport(new ValidationSupportChain(valSupport, defaultSupport)); // Prepopulate SDs valSupport.addStructureDefinition(loadStructureDefinition(defaultSupport, "/dstu3/myconsent-profile.xml")); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java index 11c23a34400..c7db3707012 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java @@ -7,13 +7,12 @@ import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; -import org.apache.commons.io.IOUtils; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport.CodeValidationResult; -import org.hl7.fhir.r4.hapi.ctx.ValidationSupportChain; +import org.hl7.fhir.r4.hapi.validation.ValidationSupportChain; import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode; @@ -23,7 +22,6 @@ import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemOptionComponent; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; -import org.hl7.fhir.r4.model.IdType; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; 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 284871ebda7..14022b28cf7 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 @@ -22,7 +22,7 @@ import ca.uhn.fhir.rest.api.SortSpec; public class ${className}ResourceProvider extends ## We have specialized base classes for RPs that handle certain resource types. These ## RPs implement type specific operations -#if ( $version != 'dstu' && (${className} == 'Encounter' || ${className} == 'Patient' || ${className} == 'ValueSet' || ${className} == 'QuestionnaireAnswers' || ${className} == 'CodeSystem' || ($version != 'dstu2' && ${className} == 'ConceptMap'))) +#if ( $version != 'dstu' && (${className} == 'Encounter' || ${className} == 'Patient' || ${className} == 'ValueSet' || ${className} == 'QuestionnaireAnswers' || ${className} == 'CodeSystem' || ($version != 'dstu2' && ${className} == 'ConceptMap') || ${className} == 'MessageHeader')) BaseJpaResourceProvider${className}${versionCapitalized} #else JpaResourceProvider${versionCapitalized}<${className}> diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans_java.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans_java.vm index 2cad6d3e9c5..979d758e85b 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans_java.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_spring_beans_java.vm @@ -26,11 +26,20 @@ import ca.uhn.fhir.jpa.dao.*; @Configuration public abstract class BaseJavaConfig${versionCapitalized} extends ca.uhn.fhir.jpa.config${package_suffix}.Base${versionCapitalized}Config { + /** + * Subclasses may override + */ + protected boolean isSupported(String theResourceType) { + return true; + } + @Bean(name="myResourceProviders${versionCapitalized}") public List<IResourceProvider> resourceProviders${versionCapitalized}() { List<IResourceProvider> retVal = new ArrayList<IResourceProvider>(); #foreach ( $res in $resources ) - retVal.add(rp${res.declaringClassNameComplete}${versionCapitalized}()); + if (isSupported("${res.name}")) { + retVal.add(rp${res.declaringClassNameComplete}${versionCapitalized}()); + } #end return retVal; } @@ -39,7 +48,9 @@ public abstract class BaseJavaConfig${versionCapitalized} extends ca.uhn.fhir.jp public List<IFhirResourceDao<?>> resourceDaos${versionCapitalized}() { List<IFhirResourceDao<?>> retVal = new ArrayList<IFhirResourceDao<?>>(); #foreach ( $res in $resources ) - retVal.add(dao${res.declaringClassNameComplete}${versionCapitalized}()); + if (isSupported("${res.name}")) { + retVal.add(dao${res.declaringClassNameComplete}${versionCapitalized}()); + } #end return retVal; } @@ -62,7 +73,7 @@ public abstract class BaseJavaConfig${versionCapitalized} extends ca.uhn.fhir.jp IFhirResourceDaoConceptMap<org.hl7.fhir.dstu3.model.ConceptMap> #elseif ( ${versionCapitalized} == 'R4' && ${res.name} == 'ConceptMap' ) IFhirResourceDaoConceptMap<org.hl7.fhir.r4.model.ConceptMap> -#elseif ( ${versionCapitalized} != 'Dstu1' && ( ${res.name} == 'Encounter' || ${res.name} == 'Everything' || ${res.name} == 'Patient' || ${res.name} == 'Subscription' || ${res.name} == 'SearchParameter')) +#elseif ( ${versionCapitalized} != 'Dstu1' && ( ${res.name} == 'Encounter' || ${res.name} == 'Everything' || ${res.name} == 'Patient' || ${res.name} == 'Subscription' || ${res.name} == 'SearchParameter' || ${res.name} == 'MessageHeader')) IFhirResourceDao${res.name}<${resourcePackage}.${res.declaringClassNameComplete}> #else IFhirResourceDao<${resourcePackage}.${res.declaringClassNameComplete}> @@ -72,7 +83,7 @@ public abstract class BaseJavaConfig${versionCapitalized} extends ca.uhn.fhir.jp ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized} retVal = new ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized}(); #elseif ( ${versionCapitalized} == 'R4' && ${res.name} == 'ConceptMap' ) ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized} retVal = new ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized}(); -#elseif ( ${versionCapitalized} != 'Dstu1' && ( ${res.name} == 'Bundle' || ${res.name} == 'Encounter' || ${res.name} == 'Everything' || ${res.name} == 'Patient' || ${res.name} == 'Subscription' || ${res.name} == 'ValueSet' || ${res.name} == 'QuestionnaireResponse' || ${res.name} == 'SearchParameter' || ${res.name} == 'CodeSystem')) +#elseif ( ${res.name} == 'Bundle' || ${res.name} == 'Encounter' || ${res.name} == 'Everything' || ${res.name} == 'Patient' || ${res.name} == 'Subscription' || ${res.name} == 'ValueSet' || ${res.name} == 'QuestionnaireResponse' || ${res.name} == 'SearchParameter' || ${res.name} == 'CodeSystem' || ${res.name} == 'MessageHeader') ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized} retVal = new ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${res.name}${versionCapitalized}(); #else ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${versionCapitalized}<${resourcePackage}.${res.declaringClassNameComplete}> retVal = new ca.uhn.fhir.jpa.dao${package_suffix}.FhirResourceDao${versionCapitalized}<${resourcePackage}.${res.declaringClassNameComplete}>(); diff --git a/pom.xml b/pom.xml index c18ee38d8d3..4e4402a2b77 100644 --- a/pom.xml +++ b/pom.xml @@ -459,6 +459,11 @@ <id>Romanow88</id> <name>Roman Doboni</name> </developer> + <developer> + <id>franktao2008</id> + <name>Frank Tao</name> + <organization>Smile CDR</organization> + </developer> </developers> <licenses> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 88846479024..995fb40d82e 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -66,6 +66,90 @@ <![CDATA[<code>setFlags(flags)</code>]]> can be used to maintain the previous behaviour. </action> + <action type="974"> + JPA server loading logic has been improved to enhance performance when + loading a large number of results in a page, or when loading multiple + search results with tags. Thanks to Frank Tao for the pull request! + </action> + <action type="add" issue="1000"> + LOINC uploader has been updated to support the new LOINC filename + scheme introduced in LOINC 2.64. Thanks to Rob Hausam for the + pull request! + </action> + <action type="add"> + In the JPA server, it is now possible for a custom search parameter + to use the + <![CDATA[<code>resolve()</code>]]> function in its path to descend into + contained resources and index fields within them. + </action> + <action type="fix" issue="1010"> + Resource loading logic for the JPA server has been optimized to + reduce the number of database round trips required when loading + search results where many of the entries have a "forced ID" (an alphanumeric + client-assigned reosurce ID). Thanks to Frank Tao for the pull + request! + </action> + <action type="add"> + A new IValidationSupport implementation has been added, named CachingValidationSupport. This + module wraps another implementation and provides short-term caching. This can have a dramatic + performance improvement on servers that are validating or executing FHIRPath repeatedly + under load. This module is used by default in the JPA server. + </action> + <action type="fix"> + An index in the JPA server on the HFJ_FORCED_ID table was incorrectly + not marked as unique. This meant that under heavy load it was possible to + create two resources with the same client-assigned ID. + </action> + <action type="fix"> + The JPA server + <![CDATA[<code>$expunge</code>]]> + operation deleted components of an individual resource record in + separate database transactions, meaning that if an operation failed + unexpectedly resources could be left in a weird state. This has been + corrected. + </action> + <action type="fix" issue="1015"> + A bug was fixed in the JPA terminology uploader, where it was possible + in some cases for some ValueSets and ConceptMaps to not be saved because + of a premature short circuit during deferred uploading. Thanks to + Joel Schneider for the pull request! + </action> + <action type="fix" issue="969"> + A bug in the HAPI FHIR CLI was fixed, where uploading terminology for R4 + could cause an error about the incorrect FHIR version. Thanks to + Rob Hausam for the pull request! + </action> + <action type="add"> + A new method has been added to AuthorizationInterceptor that can be used to + create rules allowing FHIR patch operations. See + <![CDATA[<a href="http://hapifhir.io/doc_rest_server_security.html#Authorizing_Patch_Operations">Authorizing Patch Operations</a>]]> + for more information. + </action> + <action type="add" issue="1018"> + A new view has been added to the JPA server, reducing the number of database + calls required when reading resources back. This causes an improvement in performance. + Thanks to Frank Tao for the pull request! + </action> + <action type="fix"> + A crash was fixed when deleting a ConceptMap resource in the + JPA server. This crash was a regression in HAPI FHIR 3.4.0. + </action> + <action type="fix"> + A crash in the JPA server when performing a manual reindex of a deleted resource + was fixed. + </action> + <action type="fix"> + Using the generic/fluent client, it is now possible to + invoke the $process-message method using a standard + client.operation() call. Previously this caused a strange + NullPointerException. + </action> + <action type="fix"> + The REST Server now sanitizes URL path components and query parameter + names to escape several reserved characters (e.g. " and <) + in order to prevent HTML injection attacks via maliciously + crafted URLs. + </action> </release> <release version="3.4.0" date="2018-05-28"> <action type="add"> diff --git a/src/site/xdoc/doc_rest_server_security.xml b/src/site/xdoc/doc_rest_server_security.xml index 1b02bbaba98..111ba9186ea 100644 --- a/src/site/xdoc/doc_rest_server_security.xml +++ b/src/site/xdoc/doc_rest_server_security.xml @@ -215,6 +215,29 @@ </subsection> + <subsection name="Authorizing Patch Operations"> + <p> + The FHIR <a href="http://hl7.org/fhir/http.html#patch">patch</a> operation + presents a challenge for authorization, as the incoming request often contains + very little detail about what is being modified. + </p> + <p> + In order to properly enforce authorization on a server that + allows the patch operation, a rule may be added that allows all + patch requests, as shown below. + </p> + <p> + This should be combined with server support for + <a href="http://hapifhir.io/doc_rest_server_security.html#Authorizing_Sub-Operations">Authorizing Sub-Operations</a> + as shown above. + </p> + <macro name="snippet"> + <param name="id" value="patchAll" /> + <param name="file" value="examples/src/main/java/example/AuthorizationInterceptors.java" /> + </macro> + + </subsection> + <subsection name="Authorizing Multitenant Servers"> <p>