From 218937e425b934883b99ab1e5a98c19124c72f46 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 20 Jun 2018 05:53:32 -0400 Subject: [PATCH 1/5] Several fixes --- .../ca/uhn/fhir/jpa/entity/TermConcept.java | 2 +- .../TermConceptPropertyFieldBridge.java | 22 ++++- .../search/LuceneSearchMappingFactory.java | 3 +- .../jpa/term/BaseHapiTerminologySvcImpl.java | 37 ++++++-- .../jpa/term/TerminologySvcImplDstu3Test.java | 92 +++++++++++++++++++ .../loinc/PartRelatedCodeMapping_Beta_1.csv | 21 +++-- .../src/test/resources/loinc/Part_Beta_1.csv | 1 + .../search/ElasticsearchMappingProvider.java | 4 + .../Example20_ValidateResource.java | 30 ++++++ src/changes/changes.xml | 5 + 10 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 hapi-fhir-validation/src/test/java/fluentpath/Example20_ValidateResource.java 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..013cf26d233 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 @@ -78,7 +78,7 @@ public class TermConcept implements Serializable { private String myDisplay; @OneToMany(mappedBy = "myConcept", orphanRemoval = true) - @Field + @Field(name = "PROPmyProperties", analyzer = @Analyzer(definition = "termConceptPropertyAnalyzer")) @FieldBridge(impl = TermConceptPropertyFieldBridge.class) private Collection myProperties; 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..c7092c30473 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,12 +21,16 @@ 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 */ @@ -51,13 +55,23 @@ public class TermConceptPropertyFieldBridge implements FieldBridge, StringBridge Collection properties = (Collection) theValue; if (properties != null) { for (TermConceptProperty next : properties) { - String propValue = next.getKey() + "=" + next.getValue(); - theLuceneOptions.addFieldToDocument(theName, propValue, theDocument); +// String propValue = next.getKey() + "=" + next.getValue(); +// theLuceneOptions.addFieldToDocument(theName, propValue, theDocument); +// +// if (next.getType() == TermConceptPropertyTypeEnum.CODING) { +// propValue = next.getKey() + "=" + next.getDisplay(); +// theLuceneOptions.addFieldToDocument(theName, propValue, theDocument); +// } + + theDocument.add(new StringField("PROP"+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("PROP" + next.getKey(), next.getDisplay(), Field.Store.YES)); + } +// theLuceneOptions.addFieldToDocument("PROPmyProperties", next.getKey() + "=" + next.getDisplay(), theDocument); } + } } } 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..d316902a73c 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 @@ -66,7 +66,8 @@ public class LuceneSearchMappingFactory { .analyzerDef("standardAnalyzer", StandardTokenizerFactory.class) .filter(LowerCaseFilterFactory.class) .analyzerDef("exactAnalyzer", StandardTokenizerFactory.class) - .analyzerDef("conceptParentPidsAnalyzer", WhitespaceTokenizerFactory.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/term/BaseHapiTerminologySvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java index c9c5992c09e..bfa136017cd 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 @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.term; * 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. @@ -42,7 +42,9 @@ import com.google.common.base.Stopwatch; import com.google.common.collect.ArrayListMultimap; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; +import org.apache.lucene.index.Term; import org.apache.lucene.search.Query; +import org.apache.lucene.search.RegexpQuery; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.search.jpa.FullTextEntityManager; @@ -79,6 +81,7 @@ import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static com.google.common.base.Ascii.toLowerCase; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -352,19 +355,39 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, } else { addDisplayFilterInexact(qb, bool, nextFilter); } - } else if ((nextFilter.getProperty().equals("concept") || nextFilter.getProperty().equals("code")) && nextFilter.getOp() == ValueSet.FilterOperator.ISA) { + } 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()); } - 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()); + 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 { - bool.must(qb.phrase().onField("myProperties").sentence(nextFilter.getProperty() + "=" + nextFilter.getValue()).createQuery()); + if (nextFilter.getOp() == ValueSet.FilterOperator.REGEX) { + + String value = nextFilter.getValue(); + if (value.endsWith("$")) { + value = value.substring(0,value.length() - 1); + } + Term term = new Term("PROP"+nextFilter.getProperty(), value); + RegexpQuery query = new RegexpQuery(term); + bool.must(query); + + + } else { + + bool.must(qb.phrase().onField("PROP" + nextFilter.getProperty()).sentence(nextFilter.getValue()).createQuery()); +// bool.must(qb.phrase().onField("myProperties").sentence(nextFilter.getProperty() + "=" + nextFilter.getValue()).createQuery()); + + } } } @@ -905,7 +928,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); 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..e17ee25c4dd 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 @@ -144,6 +144,98 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { } @Test + public void testExpandValueSetPropertySearchWithRegexInclude() { + 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); + }); + + 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$"); +// .setValue("\\\\^Donor$"); + outcome = myTermSvc.expandValueSet(vs); + codes = toCodesContains(outcome.getExpansion().getContains()); + assertThat(codes, containsInAnyOrder("50015-7")); + } + + + @Test + public void testExpandValueSetPropertySearchWithRegexExclude() { + 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); + + 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 testExpandValueSetPropertySearch() { createCodeSystem(); createCodeSystem2(); 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_Beta_1.csv index fec510bd3f0..2964d3029b3 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/loinc/PartRelatedCodeMapping_Beta_1.csv +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/PartRelatedCodeMapping_Beta_1.csv @@ -1,10 +1,11 @@ -"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",,, 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_Beta_1.csv index 8da9e598437..85a4fed2c90 100644 --- a/hapi-fhir-jpaserver-base/src/test/resources/loinc/Part_Beta_1.csv +++ b/hapi-fhir-jpaserver-base/src/test/resources/loinc/Part_Beta_1.csv @@ -38,3 +38,4 @@ "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" 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-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/src/changes/changes.xml b/src/changes/changes.xml index 88846479024..d0fba3eeeac 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -66,6 +66,11 @@ setFlags(flags)]]> can be used to maintain the previous behaviour. + + 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! + From 2dc445fd107c4f3a8f43b4fd16740275abcdca96 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 21 Jun 2018 10:19:14 -0400 Subject: [PATCH 2/5] ValueSet enhancements --- .../ca/uhn/fhir/jpa/dao/data/ISearchDao.java | 8 +- .../java/ca/uhn/fhir/jpa/entity/Search.java | 3 +- .../TermConceptPropertyFieldBridge.java | 22 +- .../jpa/term/BaseHapiTerminologySvcImpl.java | 343 +++++++++-------- .../jpa/term/TerminologyLoaderSvcImpl.java | 2 +- .../LoincPartRelatedCodeMappingHandler.java | 36 +- .../term/TerminologyLoaderSvcLoincTest.java | 2 +- .../jpa/term/TerminologySvcImplDstu3Test.java | 352 ++++++++++-------- .../loinc/PartRelatedCodeMapping_Beta_1.csv | 1 + 9 files changed, 426 insertions(+), 343 deletions(-) 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/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/TermConceptPropertyFieldBridge.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptPropertyFieldBridge.java index c7092c30473..30e822e0525 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 @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.entity; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,7 +36,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public class TermConceptPropertyFieldBridge implements FieldBridge, StringBridge { - public static final String PROP_PREFIX = "PROP__"; + public static final String CONCEPT_FIELD_PROPERTY_PREFIX = "PROP"; /** * Constructor @@ -52,26 +52,18 @@ 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); -// -// if (next.getType() == TermConceptPropertyTypeEnum.CODING) { -// propValue = next.getKey() + "=" + next.getDisplay(); -// theLuceneOptions.addFieldToDocument(theName, propValue, theDocument); -// } - - theDocument.add(new StringField("PROP"+next.getKey(), next.getValue(), Field.Store.YES)); + theDocument.add(new StringField(CONCEPT_FIELD_PROPERTY_PREFIX + next.getKey(), next.getValue(), Field.Store.YES)); if (next.getType() == TermConceptPropertyTypeEnum.CODING) { if (isNotBlank(next.getDisplay())) { - theDocument.add(new StringField("PROP" + next.getKey(), next.getDisplay(), Field.Store.YES)); + theDocument.add(new StringField(CONCEPT_FIELD_PROPERTY_PREFIX + next.getKey(), next.getDisplay(), Field.Store.YES)); } -// theLuceneOptions.addFieldToDocument("PROPmyProperties", next.getKey() + "=" + next.getDisplay(), theDocument); } - } } } 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 bfa136017cd..aaa1a46cc29 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,9 +40,11 @@ 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.index.Term; +import org.apache.lucene.queries.TermsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.RegexpQuery; import org.hibernate.ScrollMode; @@ -81,7 +83,6 @@ import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static com.google.common.base.Ascii.toLowerCase; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -134,10 +135,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()) { @@ -150,18 +155,32 @@ 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 removeCodeFromExpansion(String theCodeSystem, String theCode, ValueSet.ValueSetExpansionComponent theExpansionComponent) { + theExpansionComponent + .getContains() + .removeIf(t -> + theCodeSystem.equals(t.getSystem()) && + theCode.equals(t.getCode())); + } + + 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); } } @@ -299,153 +318,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")) { - - 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) { - - String value = nextFilter.getValue(); - if (value.endsWith("$")) { - value = value.substring(0,value.length() - 1); - } - Term term = new Term("PROP"+nextFilter.getProperty(), value); - RegexpQuery query = new RegexpQuery(term); - bool.must(query); - - - } else { - - bool.must(qb.phrase().onField("PROP" + nextFilter.getProperty()).sentence(nextFilter.getValue()).createQuery()); -// 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(); @@ -453,6 +336,162 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, return valueSet; } + 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()); + + /* + * Include Concepts + */ + + String codes = include + .getConcept() + .stream() + .filter(Objects::nonNull) + .map(ValueSet.ConceptReferenceComponent::getCode) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining(" ")); + if (isNotBlank(codes)) { + bool.must(qb.keyword().onField("myCode").matching(codes).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(); + 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); + } + + } + } + } + protected List expandValueSetAndReturnVersionIndependentConcepts(org.hl7.fhir.r4.model.ValueSet theValueSetToExpandR4) { org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent expandedR4 = expandValueSet(theValueSetToExpandR4).getExpansion(); 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..86dbe44b5f2 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 @@ -275,7 +275,7 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc { iterateOverZipFile(theDescriptors, LOINC_PART_LINK_FILE, handler, ',', QuoteMode.NON_NUMERIC); // Part related code mapping - handler = new LoincPartRelatedCodeMappingHandler(codeSystemVersion, code2concept, valueSets, conceptMaps, uploadProperties); + handler = new LoincPartRelatedCodeMappingHandler(code2concept, valueSets, conceptMaps, uploadProperties); iterateOverZipFile(theDescriptors, LOINC_PART_RELATED_CODE_MAPPING_FILE, handler, ',', QuoteMode.NON_NUMERIC); // Document Ontology File 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..cbc92de6680 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,20 @@ 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"; + 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 LOINC_RADLEX_PART_MAP_ID = "loinc-parts-to-radlex"; + private static final String LOINC_RADLEX_PART_MAP_URI = "http://loinc.org/cm/loinc-parts-to-radlex"; + private static final String LOINC_RADLEX_PART_MAP_NAME = "LOINC Part Map to RADLEX"; 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://pubchem.ncbi.nlm.nih.gov"; + 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 @@ -112,8 +108,16 @@ public class LoincPartRelatedCodeMappingHandler extends BaseLoincHandler impleme loincPartMapUri = LOINC_RADLEX_PART_MAP_URI; loincPartMapName = LOINC_RADLEX_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/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..909779def41 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 @@ -18,7 +18,7 @@ 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; 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 e17ee25c4dd..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(); @@ -144,98 +175,40 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { } @Test - public void testExpandValueSetPropertySearchWithRegexInclude() { - runInTransaction(()->{ - CodeSystem codeSystem = new CodeSystem(); - codeSystem.setUrl(CS_URL); - codeSystem.setContent(CodeSystemContentMode.NOTPRESENT); - IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); + public void testCreatePropertiesAndDesignationsWithDeferredConcepts() { + myDaoConfig.setDeferIndexingForCodesystemsOfSize(1); + BaseHapiTerminologySvcImpl.setForceSaveDeferredAlwaysForUnitTest(true); - ResourceTable table = myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalArgumentException::new); + createCodeSystem(); - TermCodeSystemVersion cs = new TermCodeSystemVersion(); - cs.setResource(table); + Validate.notNull(myTermSvc); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); + myTermSvc.saveDeferred(); - 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); - }); - - List codes; - ValueSet vs; - ValueSet outcome; - ValueSet.ConceptSetComponent include; - - // Include - vs = new ValueSet(); - include = vs.getCompose().addInclude(); + ValueSet vs = new ValueSet(); + ValueSet.ConceptSetComponent include = vs.getCompose().addInclude(); include.setSystem(CS_URL); - include - .addFilter() - .setProperty("SYSTEM") - .setOp(ValueSet.FilterOperator.REGEX) - .setValue(".*\\^Donor$"); -// .setValue("\\\\^Donor$"); - outcome = myTermSvc.expandValueSet(vs); - codes = toCodesContains(outcome.getExpansion().getContains()); - assertThat(codes, containsInAnyOrder("50015-7")); - } + 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 testExpandValueSetPropertySearchWithRegexExclude() { - 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); - - 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 testExpandValueSetPropertySearch() { createCodeSystem(); createCodeSystem2(); @@ -286,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(); @@ -301,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(); @@ -469,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/resources/loinc/PartRelatedCodeMapping_Beta_1.csv b/hapi-fhir-jpaserver-base/src/test/resources/loinc/PartRelatedCodeMapping_Beta_1.csv index 2964d3029b3..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_Beta_1.csv @@ -9,3 +9,4 @@ "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",,, From 024394e5e57c91cbbd632cacb0a16de73c80424e Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 21 Jun 2018 10:20:08 -0400 Subject: [PATCH 3/5] Credit for #1000 --- src/changes/changes.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index d0fba3eeeac..ad78d6ae7d6 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -71,6 +71,11 @@ 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! + + 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! + From b3f7ab274d03cc23baa6c61e30cc7e1e5318d4a0 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Thu, 21 Jun 2018 14:44:34 -0400 Subject: [PATCH 4/5] License header updates --- .../TermConceptPropertyFieldBridge.java | 4 ++-- .../jpa/term/BaseHapiTerminologySvcImpl.java | 4 ++-- .../uhn/fhir/rest/server/RestfulServer.java | 4 ++-- .../auth/AuthorizationFlagsEnum.java | 20 +++++++++++++++++++ .../auth/AuthorizationInterceptor.java | 4 ++-- .../server/interceptor/auth/RuleImplOp.java | 4 ++-- 6 files changed, 30 insertions(+), 10 deletions(-) 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 30e822e0525..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 @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.entity; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 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 aaa1a46cc29..f2bcbc67a8c 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 @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.term; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/hapi-fhir-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..d6b1caa9d79 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationFlagsEnum.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationFlagsEnum.java index c1aa44f6eb3..5a558ad9c50 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationFlagsEnum.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationFlagsEnum.java @@ -1,5 +1,25 @@ 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 java.util.Collection; /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java index bf9c88d826f..816282e347d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 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 98ae3659cef..e89012642e7 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 @@ -36,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. From e9fffd3cdc3d4366553c00afbc90b6344f444543 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 22 Jun 2018 11:22:07 -0400 Subject: [PATCH 5/5] Allow indexing in custom search params to descend into contained resources --- ...ceDaoDstu3SearchCustomSearchParamTest.java | 44 +- ...ourceDaoR4SearchCustomSearchParamTest.java | 44 +- .../hl7/fhir/dstu3/utils/FHIRPathEngine.java | 4590 ++++++------ .../org/hl7/fhir/r4/utils/FHIRPathEngine.java | 6594 +++++++++-------- .../fhir/dstu3/utils/FhirPathEngineTest.java | 46 +- .../fhir/r4/utils/FhirPathEngineR4Test.java | 21 +- src/changes/changes.xml | 6 + 7 files changed, 5834 insertions(+), 5511 deletions(-) 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/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java index d6c93c27d97..12cf0b369de 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"); @@ -175,6 +175,48 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test assertThat(ids, contains(pid.getValue())); } + @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 testExtensionWithNoValueIndexesWithoutFailure() { SearchParameter eyeColourSp = new SearchParameter(); 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-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-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 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 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 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 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 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/src/changes/changes.xml b/src/changes/changes.xml index ad78d6ae7d6..811d7593bf5 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -76,6 +76,12 @@ scheme introduced in LOINC 2.64. Thanks to Rob Hausam for the pull request! + + In the JPA server, it is now possible for a custom search parameter + to use the + resolve()]]> function in its path to descend into + contained resources and index fields within them. +