From 2e8c20dc83635ba43122b3e6af2eb9b163f4c7a2 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Mon, 20 Jun 2016 07:19:08 -0400 Subject: [PATCH] More work on terminology services, and add support to operations to AuthorizationInterceptor --- .../rest/client/RestfulClientFactory.java | 18 +- .../ResponseHighlighterInterceptor.java | 8 +- .../server/interceptor/auth/BaseRule.java | 30 ++ .../auth/IAuthRuleBuilderOperation.java | 17 + .../auth/IAuthRuleBuilderOperationNamed.java | 23 + .../auth/IAuthRuleBuilderRule.java | 5 + .../interceptor/auth/OperationRule.java | 96 ++++ .../rest/server/interceptor/auth/Rule.java | 37 +- .../server/interceptor/auth/RuleBuilder.java | 75 +++ .../server/interceptor/auth/RuleOpEnum.java | 3 +- .../main/java/ca/uhn/fhir/util/XmlUtil.java | 4 - .../ca/uhn/fhir/jpa/demo/FhirDbConfig.java | 2 +- .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 6 +- .../ca/uhn/fhir/jpa/entity/TermConcept.java | 4 +- .../entity/TermConceptParentChildLink.java | 4 + .../TerminologyUploaderProviderDstu3.java | 5 +- .../DeferConceptIndexingInterceptor.java | 32 -- .../fhir/jpa/term/BaseHapiTerminologySvc.java | 148 ++++-- .../fhir/jpa/term/IHapiTerminologySvc.java | 8 + .../fhir/jpa/term/TerminologyLoaderSvc.java | 52 +- .../FhirResourceDaoDstu3TerminologyTest.java | 19 +- .../jpa/term/TerminologyLoaderSvcTest.java | 62 ++- .../sct/sct2_Concept_Full_INT_20160131.txt | 1 + .../sct2_Relationship_Full_INT_20160131.txt | 3 +- .../rest/client/GenericClientDstu2Test.java | 485 ++++++++++-------- .../ApacheRestfulClientFactoryTest.java | 40 ++ .../AuthorizationInterceptorDstu2Test.java | 248 +++++++++ src/changes/changes.xml | 4 + 28 files changed, 1052 insertions(+), 387 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperation.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderOperationNamed.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java delete mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DeferConceptIndexingInterceptor.java create mode 100644 hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheRestfulClientFactoryTest.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java index df14bf81474..2c8cf5a5cd2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java @@ -194,7 +194,7 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory { public void validateServerBaseIfConfiguredToDoSo(String theServerBase, IHttpClient theHttpClient, BaseClient theClient) { String serverBase = normalizeBaseUrlForMap(theServerBase); - switch (myServerValidationMode) { + switch (getServerValidationMode()) { case NEVER: break; case ONCE: @@ -267,22 +267,6 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory { myPoolMaxPerRoute = thePoolMaxPerRoute; resetHttpClient(); } - - /** - * Instantiates a new client invocation handler - * @param theClient - * the client which will invoke the call - * @param theUrlBase - * the url base - * @param theMethodToReturnValue - * @param theBindings - * @param theMethodToLambda - * @return a newly created client invocation handler - */ - ClientInvocationHandler newInvocationHandler(IHttpClient theClient, String theUrlBase, Map theMethodToReturnValue, Map> theBindings, Map theMethodToLambda) { - return new ClientInvocationHandler(theClient, getFhirContext(), theUrlBase.toString(), theMethodToReturnValue, - theBindings, theMethodToLambda, this); - } @SuppressWarnings("unchecked") @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java index a7040d705fc..bfd47b2fd42 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java @@ -340,19 +340,21 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { b.append("

"); b.append("This result is being rendered in HTML for easy viewing. "); - b.append("You may view this content as "); + b.append("You may access this content as "); b.append("Raw JSON, "); + b.append("\">Raw JSON or "); b.append("Raw XML, "); + b.append(" or view this content in "); + b.append("HTML JSON, "); + b.append("\">HTML JSON "); b.append("or "); b.append("server level + */ + IAuthRuleBuilderRuleOpClassifierFinished onServer(); + + /** + * Rule applies to invocations of this operation at the type level + */ + IAuthRuleBuilderRuleOpClassifierFinished onType(Class theType); + + /** + * Rule applies to invocations of this operation at the instance level + */ + IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java index 725aff15ea2..3e324f60895 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java @@ -55,4 +55,9 @@ public interface IAuthRuleBuilderRule { */ IAuthRuleBuilderRuleOp write(); + /** + * This rule applies to a FHIR operation (e.g. $validate) + */ + IAuthRuleBuilderOperation operation(); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java new file mode 100644 index 00000000000..a4581cb4710 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java @@ -0,0 +1,96 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import java.util.HashSet; +import java.util.List; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict; + +class OperationRule extends BaseRule implements IAuthRule { + + public OperationRule(String theRuleName) { + super(theRuleName); + } + + private String myOperationName; + private boolean myAppliesToServer; + private HashSet> myAppliesToTypes; + private List myAppliesToIds; + + /** + * Must include the leading $ + */ + public void setOperationName(String theOperationName) { + myOperationName = theOperationName; + } + + public String getOperationName() { + return myOperationName; + } + + @Override + public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource, IRuleApplier theRuleApplier) { + FhirContext ctx = theRequestDetails.getServer().getFhirContext(); + + boolean applies = false; + switch (theOperation) { + case EXTENDED_OPERATION_SERVER: + if (myAppliesToServer) { + applies = true; + } + break; + case EXTENDED_OPERATION_TYPE: + if (myAppliesToTypes != null) { + for (Class next : myAppliesToTypes) { + String resName = ctx.getResourceDefinition(theRequestDetails.getResourceName()).getName(); + if (resName.equals(theRequestDetails.getResourceName())) { + applies = true; + break; + } + } + } + break; + case EXTENDED_OPERATION_INSTANCE: + if (myAppliesToIds != null) { + String instanceId = theRequestDetails.getId().toUnqualifiedVersionless().getValue(); + for (IIdType next : myAppliesToIds) { + if (next.toUnqualifiedVersionless().getValue().equals(instanceId)) { + applies = true; + break; + } + } + } + break; + default: + return null; + } + + if (!applies) { + return null; + } + + if (myOperationName != null && !myOperationName.equals(theRequestDetails.getOperation())) { + return null; + } + + return newVerdict(); + } + + public void appliesToServer() { + myAppliesToServer = true; + } + + public void appliesToTypes(HashSet> theAppliesToTypes) { + myAppliesToTypes = theAppliesToTypes; + } + + public void appliesToInstances(List theAppliesToIds) { + myAppliesToIds = theAppliesToIds; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java index 26e29efce77..ffd6fad82f6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java @@ -24,7 +24,6 @@ import java.util.Collection; import java.util.List; import java.util.Set; -import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -40,20 +39,18 @@ import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.BundleUtil.BundleEntryParts; import ca.uhn.fhir.util.FhirTerser; -class Rule implements IAuthRule { +class Rule extends BaseRule implements IAuthRule { private AppliesTypeEnum myAppliesTo; private Set myAppliesToTypes; private String myClassifierCompartmentName; private Collection myClassifierCompartmentOwners; private ClassifierTypeEnum myClassifierType; - private PolicyEnum myMode; - private String myName; private RuleOpEnum myOp; private TransactionAppliesToEnum myTransactionAppliesToOp; public Rule(String theRuleName) { - myName = theRuleName; + super(theRuleName); } @Override @@ -77,7 +74,7 @@ class Rule implements IAuthRule { case DELETE: if (theOperation == RestOperationTypeEnum.DELETE) { if (theInputResource == null) { - return new Verdict(myMode, this); + return newVerdict(); } else { appliesTo = theInputResource; } @@ -88,13 +85,13 @@ class Rule implements IAuthRule { case BATCH: case TRANSACTION: if (theInputResource != null && requestAppliesToTransaction(ctx, myOp, theInputResource)) { - if (myMode == PolicyEnum.DENY) { + if (getMode() == PolicyEnum.DENY) { return new Verdict(PolicyEnum.DENY, this); - } else { + } else { List inputResources = BundleUtil.toListOfEntries(ctx, (IBaseBundle) theInputResource); Verdict verdict = null; for (BundleEntryParts nextPart : inputResources) { - + IBaseResource inputResource = nextPart.getResource(); RestOperationTypeEnum operation = null; if (nextPart.getRequestType() == RequestTypeEnum.GET) { @@ -111,13 +108,13 @@ class Rule implements IAuthRule { /* * This is basically just being conservative - Be careful of transactions containing * nested operations and nested transactions. We block the by default. At some point - * it would be nice to be more nuanced here. + * it would be nice to be more nuanced here. */ RuntimeResourceDefinition resourceDef = ctx.getResourceDefinition(nextPart.getResource()); if ("Parameters".equals(resourceDef.getName()) || "Bundle".equals(resourceDef.getName())) { - throw new InvalidRequestException("Can not handle transaction with nested resource of type " + resourceDef.getName()); + throw new InvalidRequestException("Can not handle transaction with nested resource of type " + resourceDef.getName()); } - + Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(operation, theRequestDetails, inputResource, null); if (newVerdict == null) { continue; @@ -155,7 +152,7 @@ class Rule implements IAuthRule { return new Verdict(PolicyEnum.DENY, this); case METADATA: if (theOperation == RestOperationTypeEnum.METADATA) { - return new Verdict(myMode, this); + return newVerdict(); } else { return null; } @@ -196,14 +193,15 @@ class Rule implements IAuthRule { throw new IllegalStateException("Unable to apply security to event of applies to type " + myAppliesTo); } - return new Verdict(myMode, this); + return newVerdict(); } + private boolean requestAppliesToTransaction(FhirContext theContext, RuleOpEnum theOp, IBaseResource theInputResource) { if (!"Bundle".equals(theContext.getResourceDefinition(theInputResource).getName())) { return false; } - + IBaseBundle request = (IBaseBundle) theInputResource; String bundleType = BundleUtil.getBundleType(theContext, request); switch (theOp) { @@ -216,11 +214,6 @@ class Rule implements IAuthRule { } } - @Override - public String getName() { - return myName; - } - public TransactionAppliesToEnum getTransactionAppliesToOp() { return myTransactionAppliesToOp; } @@ -245,9 +238,6 @@ class Rule implements IAuthRule { myClassifierType = theClassifierType; } - public void setMode(PolicyEnum theRuleMode) { - myMode = theRuleMode; - } public Rule setOp(RuleOpEnum theRuleOp) { myOp = theRuleOp; @@ -257,4 +247,5 @@ class Rule implements IAuthRule { public void setTransactionAppliesToOp(TransactionAppliesToEnum theOp) { myTransactionAppliesToOp = theOp; } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 3f1027dd15f..7ffeeaa92c1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -243,6 +244,80 @@ public class RuleBuilder implements IAuthRuleBuilder { } + private class RuleBuilderRuleOperation implements IAuthRuleBuilderOperation { + + private class RuleBuilderRuleOperationNamed implements IAuthRuleBuilderOperationNamed { + + private String myOperationName; + + public RuleBuilderRuleOperationNamed(String theOperationName) { + if (theOperationName != null && !theOperationName.startsWith("$")) { + myOperationName = '$' + theOperationName; + } else { + myOperationName = theOperationName; + } + } + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished onServer() { + OperationRule rule = createRule(); + rule.appliesToServer(); + myRules.add(rule); + return new RuleBuilderFinished(); + } + + private OperationRule createRule() { + OperationRule rule = new OperationRule(myRuleName); + rule.setOperationName(myOperationName); + rule.setMode(myRuleMode); + return rule; + } + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished onType(Class theType) { + Validate.notNull(theType, "theType must not be null"); + + OperationRule rule = createRule(); + HashSet> appliesToTypes = new HashSet>(); + appliesToTypes.add(theType); + rule.appliesToTypes(appliesToTypes); + myRules.add(rule); + return new RuleBuilderFinished(); + } + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId) { + Validate.notNull(theInstanceId, "theInstanceId must not be null"); + Validate.notBlank(theInstanceId.getResourceType(), "theInstanceId does not have a resource type"); + Validate.notBlank(theInstanceId.getIdPart(), "theInstanceId does not have an ID part"); + + OperationRule rule = createRule(); + ArrayList ids = new ArrayList(); + ids.add(theInstanceId); + rule.appliesToInstances(ids); + myRules.add(rule); + return new RuleBuilderFinished(); + } + + } + + @Override + public IAuthRuleBuilderOperationNamed named(String theOperationName) { + Validate.notBlank(theOperationName, "theOperationName must not be null or empty"); + return new RuleBuilderRuleOperationNamed(theOperationName); + } + + @Override + public IAuthRuleBuilderOperationNamed withAnyName() { + return new RuleBuilderRuleOperationNamed(null); + } + + } + + @Override + public IAuthRuleBuilderOperation operation() { + return new RuleBuilderRuleOperation(); + } } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java index 3d6296424c5..b9ce9f8839f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java @@ -28,5 +28,6 @@ enum RuleOpEnum { TRANSACTION, METADATA, BATCH, - DELETE + DELETE, + OPERATION } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java index 95f998693fa..fcfc0889ee6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java @@ -1656,10 +1656,6 @@ public class XmlUtil { ourHaveLoggedStaxImplementation = true; } - public static void main(String[] args) throws FactoryConfigurationError, XMLStreamException { - createXmlWriter(new StringWriter()); - } - private static final class ExtendedEntityReplacingXmlResolver implements XMLResolver { @Override public Object resolveEntity(String thePublicID, String theSystemID, String theBaseURI, String theNamespace) throws XMLStreamException { diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirDbConfig.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirDbConfig.java index b9789438c9e..70751c6f170 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirDbConfig.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirDbConfig.java @@ -26,7 +26,7 @@ public class FhirDbConfig { extraProperties.put("hibernate.search.default.directory_provider", "filesystem"); extraProperties.put("hibernate.search.default.indexBase", "target/lucenefiles"); extraProperties.put("hibernate.search.lucene_version", "LUCENE_CURRENT"); -// extraProperties.put("hibernate.search.default.worker.execution", "async"); + extraProperties.put("hibernate.search.default.worker.execution", "async"); if (System.getProperty("lowmem") != null) { extraProperties.put("hibernate.search.autoregister_listeners", "false"); 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 e6fc5d6e859..b92f62b22cc 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 @@ -51,7 +51,7 @@ public class DaoConfig { // *** // update setter javadoc if default changes // *** - private int myDeferIndexingForCodesystemsOfSize = 100; + private int myDeferIndexingForCodesystemsOfSize = 2000; // *** // update setter javadoc if default changes // *** @@ -90,7 +90,7 @@ public class DaoConfig { * the code system will be indexed later in an incremental process in order to * avoid overwhelming Lucene with a huge number of codes in a single operation. *

- * Defaults to 100 + * Defaults to 2000 *

*/ public int getDeferIndexingForCodesystemsOfSize() { @@ -273,7 +273,7 @@ public class DaoConfig { * the code system will be indexed later in an incremental process in order to * avoid overwhelming Lucene with a huge number of codes in a single operation. *

- * Defaults to 100 + * Defaults to 2000 *

*/ public void setDeferIndexingForCodesystemsOfSize(int theDeferIndexingForCodesystemsOfSize) { 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 b3e8e86bdf0..db13d0ee3df 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 @@ -55,14 +55,12 @@ import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Fields; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.Store; -import org.hibernate.search.indexes.interceptor.DontInterceptEntityInterceptor; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; -import ca.uhn.fhir.jpa.search.DeferConceptIndexingInterceptor; //@formatter:off @Entity -@Indexed(interceptor=DeferConceptIndexingInterceptor.class) +@Indexed() @Table(name="TRM_CONCEPT", uniqueConstraints= { @UniqueConstraint(name="IDX_CONCEPT_CS_CODE", columnNames= {"CODESYSTEM_PID", "CODE"}) }, indexes= { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptParentChildLink.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptParentChildLink.java index 36174b791e1..36c15af5e9c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptParentChildLink.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptParentChildLink.java @@ -66,6 +66,10 @@ public class TermConceptParentChildLink implements Serializable { return myChild; } + public RelationshipTypeEnum getRelationshipType() { + return myRelationshipType; + } + public TermCodeSystemVersion getCodeSystem() { return myCodeSystem; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/TerminologyUploaderProviderDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/TerminologyUploaderProviderDstu3.java index 78f38b93a39..06162186e2d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/TerminologyUploaderProviderDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/TerminologyUploaderProviderDstu3.java @@ -26,7 +26,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -85,7 +84,9 @@ public class TerminologyUploaderProviderDstu3 extends BaseJpaProvider { } else if (thePackage == null || thePackage.getData() == null || thePackage.getData().length == 0) { throw new InvalidRequestException("No 'localfile' or 'package' parameter, or package had no data"); } else { - data = Arrays.asList(thePackage.getData()); + data = new ArrayList(); + data.add(thePackage.getData()); + thePackage.setData(null); } String url = theUrl != null ? theUrl.getValueAsString() : null; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DeferConceptIndexingInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DeferConceptIndexingInterceptor.java deleted file mode 100644 index eefe314a686..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DeferConceptIndexingInterceptor.java +++ /dev/null @@ -1,32 +0,0 @@ -package ca.uhn.fhir.jpa.search; - -import org.hibernate.search.indexes.interceptor.EntityIndexingInterceptor; -import org.hibernate.search.indexes.interceptor.IndexingOverride; - -import ca.uhn.fhir.jpa.entity.TermConcept; - -public class DeferConceptIndexingInterceptor implements EntityIndexingInterceptor { - - @Override - public IndexingOverride onAdd(TermConcept theEntity) { - if (theEntity.getIndexStatus() == null) { - return IndexingOverride.SKIP; - } - return IndexingOverride.APPLY_DEFAULT; - } - - @Override - public IndexingOverride onUpdate(TermConcept theEntity) { - return onAdd(theEntity); - } - - @Override - public IndexingOverride onDelete(TermConcept theEntity) { - return IndexingOverride.APPLY_DEFAULT; - } - - @Override - public IndexingOverride onCollectionUpdate(TermConcept theEntity) { - return IndexingOverride.APPLY_DEFAULT; - } -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvc.java index f163786e3ff..6953964590e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvc.java @@ -33,6 +33,7 @@ import javax.persistence.PersistenceContext; import javax.persistence.PersistenceContextType; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -67,9 +68,13 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { @Autowired protected ITermConceptDao myConceptDao; + private List myConceptLinksToSaveLater = new ArrayList(); + @Autowired private ITermConceptParentChildLinkDao myConceptParentChildLinkDao; + private List myConceptsToSaveLater = new ArrayList(); + @Autowired protected FhirContext myContext; @@ -78,6 +83,8 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { @PersistenceContext(type = PersistenceContextType.TRANSACTION) protected EntityManager myEntityManager; + + private boolean myProcessDeferred = true; private boolean addToSet(Set theSetToPopulate, TermConcept theConcept) { boolean retVal = theSetToPopulate.add(theConcept); @@ -197,37 +204,101 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { TermCodeSystemVersion csv = cs.getCurrentVersion(); return csv; } - private TermCodeSystem getCodeSystem(String theSystem) { TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(theSystem); return cs; } + + private void parentPids(TermConcept theNextConcept, Set theParentPids) { + for (TermConceptParentChildLink nextParentLink : theNextConcept.getParents()){ + TermConcept parent = nextParentLink.getParent(); + if (parent != null && theParentPids.add(parent.getId())) { + parentPids(parent, theParentPids); + } + } + } private void persistChildren(TermConcept theConcept, TermCodeSystemVersion theCodeSystem, IdentityHashMap theConceptsStack, int theTotalConcepts) { if (theConceptsStack.put(theConcept, PLACEHOLDER_OBJECT) != null) { return; } - if (theConceptsStack.size() % 1000 == 0) { + if (theConceptsStack.size() == 1 || theConceptsStack.size() % 10000 == 0) { float pct = (float) theConceptsStack.size() / (float) theTotalConcepts; - ourLog.info("Have saved {}/{} concepts ({}%), flushing", theConceptsStack.size(), theTotalConcepts, (int)( pct*100.0f)); - myConceptDao.flush(); - myConceptParentChildLinkDao.flush(); + ourLog.info("Have processed {}/{} concepts ({}%)", theConceptsStack.size(), theTotalConcepts, (int)( pct*100.0f)); } theConcept.setCodeSystem(theCodeSystem); - if (theTotalConcepts < myDaoConfig.getDeferIndexingForCodesystemsOfSize()) { - theConcept.setIndexStatus(BaseHapiFhirDao.INDEX_STATUS_INDEXED); - } + theConcept.setIndexStatus(BaseHapiFhirDao.INDEX_STATUS_INDEXED); - myConceptDao.save(theConcept); + Set parentPids = new HashSet(); + parentPids(theConcept, parentPids); + theConcept.setParentPids(parentPids); + + if (theConceptsStack.size() <= myDaoConfig.getDeferIndexingForCodesystemsOfSize()) { + myConceptDao.save(theConcept); + } else { + myConceptsToSaveLater.add(theConcept); + } + for (TermConceptParentChildLink next : theConcept.getChildren()) { persistChildren(next.getChild(), theCodeSystem, theConceptsStack, theTotalConcepts); } for (TermConceptParentChildLink next : theConcept.getChildren()) { - myConceptParentChildLinkDao.save(next); + if (theConceptsStack.size() <= myDaoConfig.getDeferIndexingForCodesystemsOfSize()) { + myConceptParentChildLinkDao.save(next); + } else { + myConceptLinksToSaveLater.add(next); + } } + + } + + private void populateVersion(TermConcept theNext, TermCodeSystemVersion theCodeSystemVersion) { + if (theNext.getCodeSystem() != null) { + return; + } + theNext.setCodeSystem(theCodeSystemVersion); + for (TermConceptParentChildLink next : theNext.getChildren()) { + populateVersion(next.getChild(), theCodeSystemVersion); + } + } + + @Scheduled(fixedRate=5000) + @Transactional(propagation=Propagation.REQUIRED) + @Override + public synchronized void saveDeferred() { + if (!myProcessDeferred || ((myConceptsToSaveLater.isEmpty() && myConceptLinksToSaveLater.isEmpty()))) { + return; + } + + int codeCount = 0, relCount = 0; + + int count = Math.min(myDaoConfig.getDeferIndexingForCodesystemsOfSize(), myConceptsToSaveLater.size()); + ourLog.info("Saving {} deferred concepts...", count); + while (codeCount < count && myConceptsToSaveLater.size() > 0) { + TermConcept next = myConceptsToSaveLater.remove(0); + myConceptDao.save(next); + codeCount++; + } + + if (codeCount == 0) { + count = Math.min(myDaoConfig.getDeferIndexingForCodesystemsOfSize(), myConceptLinksToSaveLater.size()); + ourLog.info("Saving {} deferred concept relationships...", count); + while (relCount < count && myConceptLinksToSaveLater.size() > 0) { + TermConceptParentChildLink next = myConceptLinksToSaveLater.remove(0); + myConceptParentChildLinkDao.save(next); + relCount++; + } + } + + ourLog.info("Saved {} deferred concepts ({} remain) and {} deferred relationships ({} remain)", new Object[] {codeCount, myConceptsToSaveLater.size(), relCount, myConceptLinksToSaveLater.size()}); + } + + @Override + public void setProcessDeferred(boolean theProcessDeferred) { + myProcessDeferred = theProcessDeferred; } @Override @@ -265,7 +336,7 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { ourLog.info("Validating all codes in CodeSystem for storage (this can take some time for large sets)"); // Validate the code system - IdentityHashMap conceptsStack = new IdentityHashMap(); + ArrayList conceptsStack = new ArrayList(); IdentityHashMap allConcepts = new IdentityHashMap(); int totalCodeCount = 0; for (TermConcept next : theCodeSystemVersion.getConcepts()) { @@ -281,11 +352,17 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { codeSystem.setCurrentVersion(theCodeSystemVersion); codeSystem = myCodeSystemDao.saveAndFlush(codeSystem); - ourLog.info("Saving {} concepts...", totalCodeCount); + ourLog.info("Setting codesystemversion on {} concepts...", totalCodeCount); - conceptsStack = new IdentityHashMap(); for (TermConcept next : theCodeSystemVersion.getConcepts()) { - persistChildren(next, codeSystemVersion, conceptsStack, totalCodeCount); + populateVersion(next, codeSystemVersion); + } + + ourLog.info("Saving {} concepts...", totalCodeCount); + + IdentityHashMap conceptsStack2 = new IdentityHashMap(); + for (TermConcept next : theCodeSystemVersion.getConcepts()) { + persistChildren(next, codeSystemVersion, conceptsStack2, totalCodeCount); } ourLog.info("Done saving concepts, flushing to database"); @@ -293,27 +370,6 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { myConceptDao.flush(); myConceptParentChildLinkDao.flush(); - ourLog.info("Building multi-axial hierarchy..."); - - int index = 0; - int totalParents = 0; - for (TermConcept nextConcept : conceptsStack.keySet()) { - - if (index++ % 1000 == 0) { - float pct = (float) index / (float) totalCodeCount; - ourLog.info("Have built hierarchy for {}/{} concepts - {}%", index, totalCodeCount, (int)( pct*100.0f)); - } - - Set parentPids = new HashSet(); - parentPids(nextConcept, parentPids); - nextConcept.setParentPids(parentPids); - totalParents += parentPids.size(); - - myConceptDao.save(nextConcept); - } - - ourLog.info("Done building hierarchy, found {} parents", totalParents); - /* * For now we always delete old versions.. At some point it would be nice to allow configuration to keep old versions */ @@ -326,17 +382,12 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { } ourLog.info("Done deleting old code system versions"); - } - - private void parentPids(TermConcept theNextConcept, Set theParentPids) { - for (TermConceptParentChildLink nextParentLink : theNextConcept.getParents()){ - TermConcept parent = nextParentLink.getParent(); - if (parent != null && theParentPids.add(parent.getId())) { - parentPids(parent, theParentPids); - } + + if (myConceptsToSaveLater.size() > 0 || myConceptLinksToSaveLater.size() > 0) { + ourLog.info("Note that some concept saving was deferred - still have {} concepts and {} relationships", myConceptsToSaveLater.size(), myConceptLinksToSaveLater.size()); } } - + @Override public boolean supportsSystem(String theSystem) { TermCodeSystem cs = getCodeSystem(theSystem); @@ -351,16 +402,17 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { return retVal; } - private int validateConceptForStorage(TermConcept theConcept, TermCodeSystemVersion theCodeSystem, IdentityHashMap theConceptsStack, + private int validateConceptForStorage(TermConcept theConcept, TermCodeSystemVersion theCodeSystem, ArrayList theConceptsStack, IdentityHashMap theAllConcepts) { ValidateUtil.isTrueOrThrowInvalidRequest(theConcept.getCodeSystem() != null, "CodesystemValue is null"); ValidateUtil.isTrueOrThrowInvalidRequest(theConcept.getCodeSystem() == theCodeSystem, "CodeSystems are not equal"); ValidateUtil.isNotBlankOrThrowInvalidRequest(theConcept.getCode(), "Codesystem contains a code with no code value"); - if (theConceptsStack.put(theConcept, PLACEHOLDER_OBJECT) != null) { + if (theConceptsStack.contains(theConcept.getCode())) { throw new InvalidRequestException("CodeSystem contains circular reference around code " + theConcept.getCode()); } - + theConceptsStack.add(theConcept.getCode()); + int retVal = 0; if (theAllConcepts.put(theConcept, theAllConcepts) == null) { if (theAllConcepts.size() % 1000 == 0) { @@ -374,7 +426,7 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { retVal += validateConceptForStorage(next.getChild(), theCodeSystem, theConceptsStack, theAllConcepts); } - theConceptsStack.remove(theConcept); + theConceptsStack.remove(theConceptsStack.size() - 1); return retVal; } 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 233a3d1d05a..a7c8f6815d5 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 @@ -48,4 +48,12 @@ public interface IHapiTerminologySvc { List findCodes(String theSystem); + void saveDeferred(); + + /** + * This is mostly for unit tests - we can disable processing of deferred concepts + * by changing this flag + */ + void setProcessDeferred(boolean theProcessDeferred); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvc.java index 2087ee24b44..7f4b6b18c03 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvc.java @@ -32,6 +32,7 @@ import java.io.OutputStream; import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -262,11 +263,18 @@ public class TerminologyLoaderSvc implements IHapiTerminologyLoaderSvc { ourLog.info("Have {} total concepts, {} root concepts", code2concept.size(), codeSystemVersion.getConcepts().size()); - myTermSvc.storeNewCodeSystemVersion(LOINC_URL, codeSystemVersion, theRequestDetails); + String url = LOINC_URL; + storeCodeSystem(theRequestDetails, codeSystemVersion, url); return new UploadStatistics(code2concept.size()); } + private void storeCodeSystem(RequestDetails theRequestDetails, final TermCodeSystemVersion codeSystemVersion, String url) { + myTermSvc.setProcessDeferred(false); + myTermSvc.storeNewCodeSystemVersion(url, codeSystemVersion, theRequestDetails); + myTermSvc.setProcessDeferred(true); + } + UploadStatistics processSnomedCtFiles(List theZipBytes, RequestDetails theRequestDetails) { final TermCodeSystemVersion codeSystemVersion = new TermCodeSystemVersion(); final Map id2concept = new HashMap(); @@ -289,6 +297,13 @@ public class TerminologyLoaderSvc implements IHapiTerminologyLoaderSvc { theZipBytes.clear(); + ourLog.info("Looking for root codes"); + for (Iterator> iter = rootConcepts.entrySet().iterator(); iter.hasNext(); ) { + if (iter.next().getValue().getParents().isEmpty() == false) { + iter.remove(); + } + } + ourLog.info("Done loading SNOMED CT files - {} root codes, {} total codes", rootConcepts.size(), code2concept.size()); Counter circularCounter = new Counter(); @@ -300,7 +315,8 @@ public class TerminologyLoaderSvc implements IHapiTerminologyLoaderSvc { } codeSystemVersion.getConcepts().addAll(rootConcepts.values()); - myTermSvc.storeNewCodeSystemVersion(SCT_URL, codeSystemVersion, theRequestDetails); + String url = SCT_URL; + storeCodeSystem(theRequestDetails, codeSystemVersion, url); return new UploadStatistics(code2concept.size()); } @@ -470,23 +486,33 @@ public class TerminologyLoaderSvc implements IHapiTerminologyLoaderSvc { String destinationId = theRecord.get("destinationId"); String typeId = theRecord.get("typeId"); boolean active = "1".equals(theRecord.get("active")); - if (!active) { - return; - } + TermConcept typeConcept = myCode2concept.get(typeId); TermConcept sourceConcept = myCode2concept.get(sourceId); TermConcept targetConcept = myCode2concept.get(destinationId); if (sourceConcept != null && targetConcept != null && typeConcept != null) { if (typeConcept.getDisplay().equals("Is a (attribute)")) { + RelationshipTypeEnum relationshipType = RelationshipTypeEnum.ISA; if (!sourceId.equals(destinationId)) { - TermConceptParentChildLink link = new TermConceptParentChildLink(); - link.setChild(sourceConcept); - link.setParent(targetConcept); - link.setRelationshipType(TermConceptParentChildLink.RelationshipTypeEnum.ISA); - link.setCodeSystem(myCodeSystemVersion); - myRootConcepts.remove(link.getChild().getCode()); - - targetConcept.addChild(sourceConcept, RelationshipTypeEnum.ISA); + if (active) { + TermConceptParentChildLink link = new TermConceptParentChildLink(); + link.setChild(sourceConcept); + link.setParent(targetConcept); + link.setRelationshipType(relationshipType); + link.setCodeSystem(myCodeSystemVersion); + + targetConcept.addChild(sourceConcept, relationshipType); + } else { + // not active, so we're removing any existing links + for (TermConceptParentChildLink next : new ArrayList(targetConcept.getChildren())) { + if (next.getRelationshipType() == relationshipType) { + if (next.getChild().getCode().equals(sourceConcept.getCode())) { + next.getParent().getChildren().remove(next); + next.getChild().getParents().remove(next); + } + } + } + } } } else if (ignoredTypes.contains(typeConcept.getDisplay())) { // ignore diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java index 55ca56d99f9..509b270e921 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java @@ -344,6 +344,8 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { public void testIndexingIsDeferredForLargeCodeSystems() { myDaoConfig.setDeferIndexingForCodesystemsOfSize(1); + myTermSvc.setProcessDeferred(false); + createExternalCsAndLocalVs(); ValueSet vs = new ValueSet(); @@ -353,10 +355,25 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { include.addFilter().setProperty("display").setOp(FilterOperator.ISA).setValue("ParentA"); ValueSet result = myValueSetDao.expand(vs, null); - String encoded = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result); ourLog.info(encoded); + assertEquals(0, result.getExpansion().getContains().size()); + + myTermSvc.setProcessDeferred(true); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + + result = myValueSetDao.expand(vs, null); + encoded = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result); + ourLog.info(encoded); + assertEquals(4, result.getExpansion().getContains().size()); + } @Test diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcTest.java index 668c10c185d..f8f9f9b1f3c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcTest.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.term; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInRelativeOrder; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; @@ -12,8 +13,10 @@ import static org.mockito.Mockito.verify; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.TreeSet; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -40,19 +43,19 @@ import ca.uhn.fhir.util.TestUtil; public class TerminologyLoaderSvcTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TerminologyLoaderSvcTest.class); private TerminologyLoaderSvc mySvc; - + @Mock private IHapiTerminologySvc myTermSvc; @Captor private ArgumentCaptor myCsvCaptor; - + @Before public void before() { mySvc = new TerminologyLoaderSvc(); mySvc.setTermSvcForUnitTests(myTermSvc); } - + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); @@ -62,18 +65,18 @@ public class TerminologyLoaderSvcTest { public void testLoadLoinc() throws Exception { ByteArrayOutputStream bos1 = new ByteArrayOutputStream(); ZipOutputStream zos1 = new ZipOutputStream(bos1); - addEntry(zos1,"/loinc/", "loinc.csv"); + addEntry(zos1, "/loinc/", "loinc.csv"); zos1.close(); ourLog.info("ZIP file has {} bytes", bos1.toByteArray().length); - + ByteArrayOutputStream bos2 = new ByteArrayOutputStream(); ZipOutputStream zos2 = new ZipOutputStream(bos2); - addEntry(zos2,"/loinc/", "LOINC_2.54_MULTI-AXIAL_HIERARCHY.CSV"); + addEntry(zos2, "/loinc/", "LOINC_2.54_MULTI-AXIAL_HIERARCHY.CSV"); zos2.close(); ourLog.info("ZIP file has {} bytes", bos2.toByteArray().length); - + RequestDetails details = mock(RequestDetails.class); - mySvc.loadLoinc(Arrays.asList(bos1.toByteArray(), bos2.toByteArray()), details); + mySvc.loadLoinc(list(bos1.toByteArray(), bos2.toByteArray()), details); } @Test @@ -84,38 +87,48 @@ public class TerminologyLoaderSvcTest { addEntry(zos, "/sct/", "sct2_Concept_Full-en_INT_20160131.txt"); addEntry(zos, "/sct/", "sct2_Description_Full-en_INT_20160131.txt"); addEntry(zos, "/sct/", "sct2_Identifier_Full_INT_20160131.txt"); - addEntry(zos,"/sct/", "sct2_Relationship_Full_INT_20160131.txt"); - addEntry(zos,"/sct/", "sct2_StatedRelationship_Full_INT_20160131.txt"); + addEntry(zos, "/sct/", "sct2_Relationship_Full_INT_20160131.txt"); + addEntry(zos, "/sct/", "sct2_StatedRelationship_Full_INT_20160131.txt"); addEntry(zos, "/sct/", "sct2_TextDefinition_Full-en_INT_20160131.txt"); zos.close(); - + ourLog.info("ZIP file has {} bytes", bos.toByteArray().length); - + RequestDetails details = mock(RequestDetails.class); - mySvc.loadSnomedCt(Collections.singletonList(bos.toByteArray()), details); - + mySvc.loadSnomedCt(list(bos.toByteArray()), details); + verify(myTermSvc).storeNewCodeSystemVersion(any(String.class), myCsvCaptor.capture(), any(RequestDetails.class)); - + TermCodeSystemVersion csv = myCsvCaptor.getValue(); - TreeSet allCodes = toCodes(csv); + TreeSet allCodes = toCodes(csv, true); ourLog.info(allCodes.toString()); - + assertThat(allCodes, containsInRelativeOrder("116680003")); assertThat(allCodes, not(containsInRelativeOrder("207527008"))); + + allCodes = toCodes(csv, false); + ourLog.info(allCodes.toString()); + assertThat(allCodes, hasItem("126816002")); } - private TreeSet toCodes(TermCodeSystemVersion theCsv) { + private List list(byte[]... theByteArray) { + return new ArrayList(Arrays.asList(theByteArray)); + } + + private TreeSet toCodes(TermCodeSystemVersion theCsv, boolean theAddChildren) { TreeSet retVal = new TreeSet(); for (TermConcept next : theCsv.getConcepts()) { - toCodes(retVal, next); + toCodes(retVal, next, theAddChildren); } return retVal; } - private void toCodes(TreeSet theCodes, TermConcept theConcept) { + private void toCodes(TreeSet theCodes, TermConcept theConcept, boolean theAddChildren) { theCodes.add(theConcept.getCode()); - for (TermConceptParentChildLink next : theConcept.getChildren()) { - toCodes(theCodes, next.getChild()); + if (theAddChildren) { + for (TermConceptParentChildLink next : theConcept.getChildren()) { + toCodes(theCodes, next.getChild(), theAddChildren); + } } } @@ -125,9 +138,9 @@ public class TerminologyLoaderSvcTest { ZipOutputStream zos = new ZipOutputStream(bos); addEntry(zos, "/sct/", "sct2_StatedRelationship_Full_INT_20160131.txt"); zos.close(); - + ourLog.info("ZIP file has {} bytes", bos.toByteArray().length); - + RequestDetails details = mock(RequestDetails.class); try { mySvc.loadSnomedCt(Collections.singletonList(bos.toByteArray()), details); @@ -145,6 +158,5 @@ public class TerminologyLoaderSvcTest { zos.write(byteArray); zos.closeEntry(); } - } diff --git a/hapi-fhir-jpaserver-base/src/test/resources/sct/sct2_Concept_Full_INT_20160131.txt b/hapi-fhir-jpaserver-base/src/test/resources/sct/sct2_Concept_Full_INT_20160131.txt index 83d9d806fc6..43be7c43d81 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/sct/sct2_Concept_Full_INT_20160131.txt +++ b/hapi-fhir-jpaserver-base/src/test/resources/sct/sct2_Concept_Full_INT_20160131.txt @@ -13,6 +13,7 @@ id effectiveTime active moduleId definitionStatusId 126813005 20020131 1 900000000000207008 900000000000074008 126813006 20020131 1 900000000000207008 900000000000074008 126817006 20020131 1 900000000000207008 900000000000074008 +126816002 20020131 1 900000000000207008 900000000000074008 207527008 20020131 1 900000000000207008 900000000000074008 207527008 20040731 1 900000000000207008 900000000000073002 207527008 20090731 0 900000000000207008 900000000000074008 diff --git a/hapi-fhir-jpaserver-base/src/test/resources/sct/sct2_Relationship_Full_INT_20160131.txt b/hapi-fhir-jpaserver-base/src/test/resources/sct/sct2_Relationship_Full_INT_20160131.txt index 98bb0b2d991..7132f3dbfe5 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/sct/sct2_Relationship_Full_INT_20160131.txt +++ b/hapi-fhir-jpaserver-base/src/test/resources/sct/sct2_Relationship_Full_INT_20160131.txt @@ -1,5 +1,6 @@ id effectiveTime active moduleId sourceId destinationId relationshipGroup typeId characteristicTypeId modifierId 100022 20020131 1 900000000000207008 126815003 126813005 0 116680003 900000000000011006 900000000000451002 -100022 20090731 0 900000000000207008 126816002 126813005 0 116680003 900000000000011006 900000000000451002 +100025 20020131 1 900000000000207008 126816002 126813005 0 116680003 900000000000011006 900000000000451002 +100025 20090731 0 900000000000207008 126816002 126813005 0 116680003 900000000000011006 900000000000451002 101021 20020131 1 900000000000207008 126817006 126815003 0 116680003 900000000000011006 900000000000451002 101021 20020131 1 900000000000207008 126815003 126817006 0 116680003 900000000000011006 900000000000451002 diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java index fcd36e88b55..653529f2c2d 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java @@ -46,6 +46,7 @@ import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.ExtensionDt; @@ -77,6 +78,8 @@ import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.client.apache.ApacheRestfulClientFactory; +import ca.uhn.fhir.rest.client.api.IHttpClient; +import ca.uhn.fhir.rest.client.api.IRestfulClient; import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.method.SearchStyleEnum; @@ -88,42 +91,24 @@ import ca.uhn.fhir.util.TestUtil; public class GenericClientDstu2Test { private static FhirContext ourCtx; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GenericClientDstu2Test.class); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GenericClientDstu2Test.class); private HttpClient myHttpClient; private HttpResponse myHttpResponse; private int myResponseCount = 0; - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - - @Test - public void testReadForUnknownType() throws Exception { - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - try { - client.read(new UriDt("1")); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("The given URI is not an absolute URL and is not usable for this operation: 1", e.getMessage()); - } - - try { - client.read(new UriDt("http://example.com/InvalidResource/1")); - fail(); - } catch (DataFormatException e) { - assertEquals("Unknown resource name \"InvalidResource\" (this name is not known in FHIR version \"DSTU2\")", e.getMessage()); - } - } - @Before public void before() { myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); ourCtx.setRestfulClientFactory(new ApacheRestfulClientFactory(ourCtx)); ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient); + ourCtx.getRestfulClientFactory().setConnectionRequestTimeout(10000); + ourCtx.getRestfulClientFactory().setConnectTimeout(10000); + ourCtx.getRestfulClientFactory().setPoolMaxPerRoute(100); + ourCtx.getRestfulClientFactory().setPoolMaxTotal(100); + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); myResponseCount = 0; @@ -156,7 +141,7 @@ public class GenericClientDstu2Test { //@formatter:on return msg; } - + @Test public void testAcceptHeaderFetchConformance() throws Exception { IParser p = ourCtx.newXmlParser(); @@ -199,68 +184,6 @@ public class GenericClientDstu2Test { idx++; } - /** - * See #322 - */ - @Test - public void testFetchConformanceWithSmartExtensions() throws Exception { - final String respString = IOUtils.toString(GenericClientDstu2Test.class.getResourceAsStream("/conformance_322.json")); - ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { - @Override - public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); - } - }); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:8080/fhir"); - Conformance conf = client.fetchConformance().ofType(Conformance.class).execute(); - - Rest rest = conf.getRest().get(0); - RestSecurity security = rest.getSecurity(); - - List ext = security.getUndeclaredExtensionsByUrl("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"); - List tokenExts = ext.get(0).getUndeclaredExtensionsByUrl("token"); - ExtensionDt tokenExt = tokenExts.get(0); - UriDt value = (UriDt) tokenExt.getValue(); - assertEquals("https://my-server.org/token", value.getValueAsString()); - - } - - /** - * See #322 - */ - @Test - public void testFetchConformanceWithSmartExtensionsAltCase() throws Exception { - final String respString = IOUtils.toString(GenericClientDstu2Test.class.getResourceAsStream("/conformance_322.json")).replace("valueuri", "valueUri"); - ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { - @Override - public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); - } - }); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:8080/fhir"); - Conformance conf = client.fetchConformance().ofType(Conformance.class).execute(); - - Rest rest = conf.getRest().get(0); - RestSecurity security = rest.getSecurity(); - - List ext = security.getUndeclaredExtensionsByUrl("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"); - List tokenExts = ext.get(0).getUndeclaredExtensionsByUrl("token"); - ExtensionDt tokenExt = tokenExts.get(0); - UriDt value = (UriDt) tokenExt.getValue(); - assertEquals("https://my-server.org/token", value.getValueAsString()); - - } - @Test public void testAcceptHeaderPreflightConformance() throws Exception { String methodName = "testAcceptHeaderPreflightConformance"; @@ -560,6 +483,30 @@ public class GenericClientDstu2Test { assertEquals("Patient/123", output.getResource().getIdElement().toUnqualifiedVersionless().getValue()); } + @Test + public void testDeleteByResource() throws Exception { + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_204_NO_CONTENT, "")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int idx = 0; + + Patient pat = new Patient(); + pat.setId("Patient/123"); + + client.delete().resource(pat).execute(); + assertEquals("DELETE", capt.getAllValues().get(idx).getMethod()); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(idx).getURI().toString()); + } + @Test public void testDeleteConditional() throws Exception { ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); @@ -595,59 +542,6 @@ public class GenericClientDstu2Test { } - @SuppressWarnings("deprecation") - @Test - public void testDeleteNonFluent() throws Exception { - ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_204_NO_CONTENT, "")); - when(myHttpResponse.getEntity().getContent()).then(new Answer() { - @Override - public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8")); - } - }); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - int idx = 0; - - client.delete(Patient.class, new IdDt("Patient/123")); - assertEquals("DELETE", capt.getAllValues().get(idx).getMethod()); - assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - client.delete(Patient.class, "123"); - assertEquals("DELETE", capt.getAllValues().get(idx).getMethod()); - assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - } - - @Test - public void testDeleteByResource() throws Exception { - ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_204_NO_CONTENT, "")); - when(myHttpResponse.getEntity().getContent()).then(new Answer() { - @Override - public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8")); - } - }); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - - int idx = 0; - - Patient pat = new Patient(); - pat.setId("Patient/123"); - - client.delete().resource(pat).execute(); - assertEquals("DELETE", capt.getAllValues().get(idx).getMethod()); - assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(idx).getURI().toString()); - } - @Test public void testDeleteInvalidRequest() throws Exception { Patient pat = new Patient(); @@ -691,6 +585,97 @@ public class GenericClientDstu2Test { } } + @SuppressWarnings("deprecation") + @Test + public void testDeleteNonFluent() throws Exception { + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_204_NO_CONTENT, "")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + int idx = 0; + + client.delete(Patient.class, new IdDt("Patient/123")); + assertEquals("DELETE", capt.getAllValues().get(idx).getMethod()); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + client.delete(Patient.class, "123"); + assertEquals("DELETE", capt.getAllValues().get(idx).getMethod()); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + } + + /** + * See #322 + */ + @Test + public void testFetchConformanceWithSmartExtensions() throws Exception { + final String respString = IOUtils.toString(GenericClientDstu2Test.class.getResourceAsStream("/conformance_322.json")); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:8080/fhir"); + Conformance conf = client.fetchConformance().ofType(Conformance.class).execute(); + + Rest rest = conf.getRest().get(0); + RestSecurity security = rest.getSecurity(); + + List ext = security.getUndeclaredExtensionsByUrl("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"); + List tokenExts = ext.get(0).getUndeclaredExtensionsByUrl("token"); + ExtensionDt tokenExt = tokenExts.get(0); + UriDt value = (UriDt) tokenExt.getValue(); + assertEquals("https://my-server.org/token", value.getValueAsString()); + + } + + /** + * See #322 + */ + @Test + public void testFetchConformanceWithSmartExtensionsAltCase() throws Exception { + final String respString = IOUtils.toString(GenericClientDstu2Test.class.getResourceAsStream("/conformance_322.json")).replace("valueuri", "valueUri"); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:8080/fhir"); + Conformance conf = client.fetchConformance().ofType(Conformance.class).execute(); + + Rest rest = conf.getRest().get(0); + RestSecurity security = rest.getSecurity(); + + List ext = security.getUndeclaredExtensionsByUrl("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"); + List tokenExts = ext.get(0).getUndeclaredExtensionsByUrl("token"); + ExtensionDt tokenExt = tokenExts.get(0); + UriDt value = (UriDt) tokenExt.getValue(); + assertEquals("https://my-server.org/token", value.getValueAsString()); + + } + @Test public void testHistory() throws Exception { @@ -796,6 +781,16 @@ public class GenericClientDstu2Test { idx++; } + @Test + public void testInvalidClient() { + try { + ourCtx.getRestfulClientFactory().newClient(RestfulClientInstance.class, "http://foo"); + fail(); + } catch (ConfigurationException e) { + assertEquals("ca.uhn.fhir.context.ConfigurationException: ca.uhn.fhir.rest.client.GenericClientDstu2Test.RestfulClientInstance is not an interface", e.toString()); + } + } + @Test public void testMetaAdd() throws Exception { IParser p = ourCtx.newXmlParser(); @@ -1663,6 +1658,24 @@ public class GenericClientDstu2Test { assertEquals("FAM", response.getName().get(0).getFamily().get(0).getValue()); } + @Test + public void testReadForUnknownType() throws Exception { + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + try { + client.read(new UriDt("1")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("The given URI is not an absolute URL and is not usable for this operation: 1", e.getMessage()); + } + + try { + client.read(new UriDt("http://example.com/InvalidResource/1")); + fail(); + } catch (DataFormatException e) { + assertEquals("Unknown resource name \"InvalidResource\" (this name is not known in FHIR version \"DSTU2\")", e.getMessage()); + } + } + @Test public void testReadUpdatedHeaderDoesntOverwriteResourceValue() throws Exception { @@ -1786,6 +1799,76 @@ public class GenericClientDstu2Test { } + @Test + public void testSearchByNumber() throws Exception { + final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + //@formatter:off + client.search() + .forResource("Encounter") + .where(Encounter.LENGTH.greaterThan().number(123)) + .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Encounter?length=gt123", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + //@formatter:off + client.search() + .forResource("Encounter") + .where(Encounter.LENGTH.lessThan().number(123)) + .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Encounter?length=lt123", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + //@formatter:off + client.search() + .forResource("Encounter") + .where(Encounter.LENGTH.greaterThanOrEqual().number("123")) + .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Encounter?length=ge123", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + //@formatter:off + client.search() + .forResource("Encounter") + .where(Encounter.LENGTH.lessThanOrEqual().number("123")) + .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Encounter?length=le123", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + //@formatter:off + client.search() + .forResource("Encounter") + .where(Encounter.LENGTH.exactly().number(123)) + .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Encounter?length=123", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + } + @Test public void testSearchByPost() throws Exception { String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; @@ -1892,76 +1975,6 @@ public class GenericClientDstu2Test { } - @Test - public void testSearchByNumber() throws Exception { - final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; - - ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).then(new Answer() { - @Override - public InputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); - } - }); - - IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); - int idx = 0; - - //@formatter:off - client.search() - .forResource("Encounter") - .where(Encounter.LENGTH.greaterThan().number(123)) - .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) - .execute(); - //@formatter:on - assertEquals("http://example.com/fhir/Encounter?length=gt123", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - //@formatter:off - client.search() - .forResource("Encounter") - .where(Encounter.LENGTH.lessThan().number(123)) - .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) - .execute(); - //@formatter:on - assertEquals("http://example.com/fhir/Encounter?length=lt123", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - //@formatter:off - client.search() - .forResource("Encounter") - .where(Encounter.LENGTH.greaterThanOrEqual().number("123")) - .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) - .execute(); - //@formatter:on - assertEquals("http://example.com/fhir/Encounter?length=ge123", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - //@formatter:off - client.search() - .forResource("Encounter") - .where(Encounter.LENGTH.lessThanOrEqual().number("123")) - .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) - .execute(); - //@formatter:on - assertEquals("http://example.com/fhir/Encounter?length=le123", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - //@formatter:off - client.search() - .forResource("Encounter") - .where(Encounter.LENGTH.exactly().number(123)) - .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) - .execute(); - //@formatter:on - assertEquals("http://example.com/fhir/Encounter?length=123", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - } - @Test public void testSearchByUrl() throws Exception { @@ -2630,9 +2643,61 @@ public class GenericClientDstu2Test { return (OperationOutcome) theOperationOutcome; } + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + @BeforeClass public static void beforeClass() { ourCtx = FhirContext.forDstu2(); } + public final static class RestfulClientInstance implements IRestfulClient { + @Override + public T fetchResourceFromUrl(Class theResourceType, String theUrl) { + return null; + } + + @Override + public FhirContext getFhirContext() { + return null; + } + + @Override + public IHttpClient getHttpClient() { + return null; + } + + @Override + public String getServerBase() { + return null; + } + + @Override + public void registerInterceptor(IClientInterceptor theInterceptor) { + //nothing + } + + @Override + public void setEncoding(EncodingEnum theEncoding) { + //nothing + } + + @Override + public void setPrettyPrint(Boolean thePrettyPrint) { + //nothing + } + + @Override + public void setSummary(SummaryEnum theSummary) { + //nothing + } + + @Override + public void unregisterInterceptor(IClientInterceptor theInterceptor) { + //nothing + } + } + } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheRestfulClientFactoryTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheRestfulClientFactoryTest.java new file mode 100644 index 00000000000..d26affcbe6e --- /dev/null +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheRestfulClientFactoryTest.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.rest.client.apache; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.BaseClient; +import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; + +public class ApacheRestfulClientFactoryTest { + + @Test + public void testSetContext() { + ApacheRestfulClientFactory factory = new ApacheRestfulClientFactory(); + factory.getServerValidationModeEnum(); + factory.setFhirContext(FhirContext.forDstu2()); + try { + factory.setFhirContext(FhirContext.forDstu2()); + fail(); + } catch (IllegalStateException e) { + assertEquals("java.lang.IllegalStateException: RestfulClientFactory instance is already associated with one FhirContext. RestfulClientFactory instances can not be shared.", e.toString()); + } + } + + @Test + public void testValidatateBase() { + FhirContext ctx = FhirContext.forDstu2(); + ApacheRestfulClientFactory factory = new ApacheRestfulClientFactory(); + factory.setFhirContext(ctx); + factory.setConnectTimeout(1); + try { + factory.validateServerBase("http://127.0.0.1:22225", factory.getHttpClient("http://foo"), (BaseClient) ctx.newRestfulGenericClient("http://foo")); + fail(); + } catch (FhirClientConnectionException e) { + assertEquals("Failed to retrieve the server metadata statement during client initialization. URL used was http://127.0.0.1:22225metadata", e.getMessage()); + } + } +} 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 f7c96863059..307e3da1217 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 @@ -40,6 +40,7 @@ import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt; import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.Parameters; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum; import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; @@ -47,6 +48,7 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Delete; import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Search; @@ -158,6 +160,215 @@ public class AuthorizationInterceptorDstu2Test { assertTrue(ourHitMethod); } + @Test + public void testOperationServerLevel() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("RULE 1").operation().named("opName").onServer().andThen() + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + // Server + ourHitMethod = false; + ourReturn = Arrays.asList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Type + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Instance + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + } + + @Test + public void testOperationInstanceLevel() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("RULE 1").operation().named("opName").onInstance(new IdDt("http://example.com/Patient/1/_history/2")).andThen() + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + // Server + ourHitMethod = false; + ourReturn = Arrays.asList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Type + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Instance + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Wrong instance + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + } + + @Test + public void testOperationAnyName() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("RULE 1").operation().withAnyName().onServer().andThen() + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + // Server + ourHitMethod = false; + ourReturn = Arrays.asList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + } + + @Test + public void testOperationTypeLevel() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("RULE 1").operation().named("opName").onType(Patient.class).andThen() + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + // Server + ourHitMethod = false; + ourReturn = Arrays.asList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Type + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Wrong type + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/1/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Wrong name + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName2"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Instance + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + } + @Test public void testDenyAll() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -762,6 +973,17 @@ public class AuthorizationInterceptorDstu2Test { retVal.setResource(theResource); return retVal; } + @Operation(name="opName", idempotent=true) + public Parameters operation() { + ourHitMethod = true; + return (Parameters) new Parameters().setId("1"); + } + @Operation(name="opName", idempotent=true) + public Parameters operation(@IdParam IdDt theId) { + ourHitMethod = true; + return (Parameters) new Parameters().setId("1"); + } + } @@ -813,11 +1035,37 @@ public class AuthorizationInterceptorDstu2Test { retVal.setResource(theResource); return retVal; } + + @Operation(name="opName", idempotent=true) + public Parameters operation() { + ourHitMethod = true; + return (Parameters) new Parameters().setId("1"); + } + + @Operation(name="opName", idempotent=true) + public Parameters operation(@IdParam IdDt theId) { + ourHitMethod = true; + return (Parameters) new Parameters().setId("1"); + } + + @Operation(name="opName2", idempotent=true) + public Parameters operation2(@IdParam IdDt theId) { + ourHitMethod = true; + return (Parameters) new Parameters().setId("1"); + } } public static class PlainProvider { + + @Operation(name="opName", idempotent=true) + public Parameters operation() { + ourHitMethod = true; + return (Parameters) new Parameters().setId("1"); + } + + @Transaction() public Bundle search(@TransactionParam Bundle theInput) { ourHitMethod = true; diff --git a/src/changes/changes.xml b/src/changes/changes.xml index f2613e46cdb..3e01399986f 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -377,6 +377,10 @@ response message --> + + AuthorizationInterceptor can now allow or deny requests to extended + operations (e.g. $everything) +