diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/IgPackUploader.java b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/IgPackUploader.java index 3b8c70dd639..d69ee208acf 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/IgPackUploader.java +++ b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/IgPackUploader.java @@ -73,6 +73,7 @@ public class IgPackUploader extends BaseCommand { .and(StructureDefinition.URL.matches().value(nextResourceUrl)) .execute(); } + break; default: throw new ParseException("This command does not support FHIR version " + ctx.getVersion().getVersion()); } diff --git a/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/VersionConvertor_10_40.java b/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/VersionConvertor_10_40.java index b3df974c78a..c44c1878063 100644 --- a/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/VersionConvertor_10_40.java +++ b/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/VersionConvertor_10_40.java @@ -1,5 +1,25 @@ package org.hl7.fhir.convertors; +/*- + * #%L + * HAPI FHIR - Converter + * %% + * Copyright (C) 2014 - 2017 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.ArrayList; import java.util.List; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/ConnectionWrapper.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/ConnectionWrapper.java index fe5fc0c9515..cd9fc5b444d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/ConnectionWrapper.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/ConnectionWrapper.java @@ -97,7 +97,7 @@ public class ConnectionWrapper implements Connection { @Override public String getClientInfo(String theName) throws SQLException { - return getClientInfo(theName); + return myWrap.getClientInfo(theName); } @Override diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index 2fd5b5fe8b1..fb83d21174f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -118,8 +118,13 @@ public abstract class BaseJpaTest { protected List toUnqualifiedVersionlessIdValues(IBundleProvider theFound) { List retVal = new ArrayList(); - int size = theFound.size(); + Integer size = theFound.size(); ourLog.info("Found {} results", size); + + if (size == null) { + size = 99999; + } + List resources = theFound.getResources(0, size); for (IBaseResource next : resources) { retVal.add(next.getIdElement().toUnqualifiedVersionless().getValue()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java index 76ffd1441f2..0c1e762798b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java @@ -102,6 +102,7 @@ public class FhirResourceDaoDstu2ValidateTest extends BaseJpaDstu2Test { } catch (PreconditionFailedException e) { return (OperationOutcome) e.getOperationOutcome(); } + break; case XML: encoded = myFhirCtx.newXmlParser().encodeResourceToString(input); try { @@ -110,6 +111,7 @@ public class FhirResourceDaoDstu2ValidateTest extends BaseJpaDstu2Test { } catch (PreconditionFailedException e) { return (OperationOutcome) e.getOperationOutcome(); } + break; } throw new IllegalStateException(); // shouldn't get here diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java index 47582dada07..3442f7cc87d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java @@ -133,6 +133,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { } catch (PreconditionFailedException e) { return (OperationOutcome) e.getOperationOutcome(); } + break; case XML: encoded = myFhirCtx.newXmlParser().encodeResourceToString(input); try { @@ -141,6 +142,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { } catch (PreconditionFailedException e) { return (OperationOutcome) e.getOperationOutcome(); } + break; } throw new IllegalStateException(); // shouldn't get here diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index 601d944d3c3..1bb289d2e03 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -133,6 +133,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { } catch (PreconditionFailedException e) { return (OperationOutcome) e.getOperationOutcome(); } + break; case XML: encoded = myFhirCtx.newXmlParser().encodeResourceToString(input); try { @@ -141,6 +142,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { } catch (PreconditionFailedException e) { return (OperationOutcome) e.getOperationOutcome(); } + break; } throw new IllegalStateException(); // shouldn't get here diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/conformance/ProfileUtilities.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/conformance/ProfileUtilities.java index e386274b8d4..8e75cf1995a 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/conformance/ProfileUtilities.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/conformance/ProfileUtilities.java @@ -1772,8 +1772,9 @@ public class ProfileUtilities extends TranslatingUtilities { case BUNDLED : return "b"; case CONTAINED : return "c"; case REFERENCED: return "r"; + default: return "?"; } - return "?"; + } @@ -2232,7 +2233,7 @@ public class ProfileUtilities extends TranslatingUtilities { if (definition != null && definition.hasShort()) { if (!c.getPieces().isEmpty()) c.addPiece(gen.new Piece("br")); c.addPiece(checkForNoChange(definition.getShortElement(), gen.new Piece(null, gt(definition.getShortElement()), null))); - } else if (fallback != null && fallback != null && fallback.hasShort()) { + } else if (fallback != null && fallback.hasShort()) { if (!c.getPieces().isEmpty()) c.addPiece(gen.new Piece("br")); c.addPiece(checkForNoChange(fallback.getShortElement(), gen.new Piece(null, gt(fallback.getShortElement()), null))); } 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 5cbb6218d55..5dadf69c830 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 @@ -1,2824 +1,2825 @@ -package org.hl7.fhir.dstu3.utils; - -import java.math.BigDecimal; -import java.util.*; - -import org.hl7.fhir.dstu3.context.IWorkerContext; -import org.hl7.fhir.dstu3.model.*; -import org.hl7.fhir.dstu3.model.ElementDefinition.TypeRefComponent; -import org.hl7.fhir.dstu3.model.ExpressionNode.*; -import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionKind; -import org.hl7.fhir.dstu3.model.StructureDefinition.TypeDerivationRule; -import org.hl7.fhir.dstu3.model.TypeDetails.ProfiledType; -import org.hl7.fhir.dstu3.utils.FHIRLexer.FHIRLexerException; -import org.hl7.fhir.dstu3.utils.FHIRPathEngine.IEvaluationContext.FunctionDetails; -import org.hl7.fhir.exceptions.*; -import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.ucum.Decimal; - -import ca.uhn.fhir.model.api.TemporalPrecisionEnum; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.util.ElementUtil; - -/** - * - * @author Grahame Grieve - * - */ -public class FHIRPathEngine { - 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 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) - */ - public FHIRPathEngine(IWorkerContext worker) { - super(); - this.worker = worker; - for (StructureDefinition sd : worker.allStructures()) { - if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) - allTypes.put(sd.getName(), sd); - if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) { - primitiveTypes.add(sd.getName()); - } - } - } - - - // --- 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 - * - * @param path - * @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 - * @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('.')); - if (Utilities.isAbsoluteUrl(resourceType)) { - 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()); - } - } - - 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.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) - 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 { - types = new TypeDetails(CollectionStatus.SINGLETON); - for (TypeRefComponent t : ed.getDefinition().getType()) - types.addType(t.getCode()); - } - } - - return executeType(new ExecutionTypeContext(appContext, sd.getUrl(), context, types), types, expr, true); - } - - 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 - 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 TypeDetails check(Object appContext, String resourceType, String context, String expr) throws FHIRLexerException, PathEngineException, DefinitionException { - return check(appContext, resourceType, context, parse(expr)); - } - - - /** - * 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); - } - - /** - * 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 - * @param path - the FHIR Path statement to use - * @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 - * @param path - the FHIR Path statement to use - * @return - * @throws FHIRException - * @ - */ - public boolean evaluateToBoolean(Object appInfo, Resource 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 - * @param path - the FHIR Path statement to use - * @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)); - } - - /** - * 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; - else - log.append(","); - log.append(convertToString(b)); - } - } - } - - 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(); - } - 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 ExpressionNode gatherPrecedence(FHIRLexer lexer, ExpressionNode start, EnumSet ops) { - // work : boolean; - // focus, node, group : ExpressionNode; - - assert(start.isProximal()); - - // is there anything to do? - boolean work = false; - ExpressionNode focus = start.getOpNext(); - if (ops.contains(start.getOperation())) { - while (focus != null && focus.getOperation() != null) { - work = work || !ops.contains(focus.getOperation()); - focus = focus.getOpNext(); - } - } else { - while (focus != null && focus.getOperation() != null) { - work = work || ops.contains(focus.getOperation()); - focus = focus.getOpNext(); - } - } - if (!work) - return start; - - // entry point: tricky - ExpressionNode group; - if (ops.contains(start.getOperation())) { - group = newGroup(lexer, start); - group.setProximal(true); - focus = start; - start = group; - } else { - ExpressionNode node = start; - - focus = node.getOpNext(); - while (!ops.contains(focus.getOperation())) { - node = focus; - focus = focus.getOpNext(); - } - group = newGroup(lexer, focus); - node.setOpNext(group); - } - - // now, at this point: - // group is the group we are adding to, it already has a .group property filled out. - // focus points at the group.group - do { - // run until we find the end of the sequence - while (ops.contains(focus.getOperation())) - focus = focus.getOpNext(); - if (focus.getOperation() != null) { - group.setOperation(focus.getOperation()); - group.setOpNext(focus.getOpNext()); - focus.setOperation(null); - focus.setOpNext(null); - // now look for another sequence, and start it - ExpressionNode node = group; - focus = group.getOpNext(); - 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 - group = newGroup(lexer, focus); - node.setOpNext(group); - } - } - } - } - while (focus != null && focus.getOperation() != null); - return start; - } - - - 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 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++; - } - } - } - - // 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 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 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); - } - - 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 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) { - List res = new ArrayList(); - res.add(new BooleanType(b)); - 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); - } - 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); - 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()); - } - } - - private List opAs(List left, List right) { - List result = new ArrayList(); - if (left.size() != 1 || right.size() != 1) - return result; - else { - String tn = convertToString(right); - if (tn.equals(left.get(0).fhirType())) - result.add(left.get(0)); - } - return result; - } - - - 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 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, ""); - 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 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.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 Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); - 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); - - 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(l.primitiveValue().compareTo(r.primitiveValue()) < 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(l.primitiveValue().compareTo(r.primitiveValue()) > 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(l.primitiveValue().compareTo(r.primitiveValue()) <= 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(l.primitiveValue().compareTo(r.primitiveValue()) >= 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 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 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()) - 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()).multiply(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 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())); - 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 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 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"); - else if (constant.equals("false")) - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - else if (Utilities.isInteger(constant)) - return new TypeDetails(CollectionStatus.SINGLETON, "integer"); - else if (Utilities.isDecimal(constant)) - return new TypeDetails(CollectionStatus.SINGLETON, "decimal"); - else if (constant.startsWith("%")) - return resolveConstantType(context, constant); - else - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - } - - private TypeDetails resolveConstantType(ExecutionTypeContext context, String s) throws PathEngineException { - if (s.equals("%sct")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.equals("%loinc")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.equals("%ucum")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.equals("%resource")) { - if (context.resource == null) - throw new PathEngineException("%resource cannot be used in this context"); - return new TypeDetails(CollectionStatus.SINGLETON, context.resource); - } else if (s.equals("%context")) { - return new TypeDetails(CollectionStatus.SINGLETON, context.context); - } else if (s.equals("%map-codes")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.equals("%us-zip")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.startsWith("%\"vs-")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.startsWith("%\"cs-")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.startsWith("%\"ext-")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (hostServices == null) - 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); - } - - - @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 ""; - String s = log.toString(); - log = new StringBuilder(); - return s; - } - -} +package org.hl7.fhir.dstu3.utils; + +import java.math.BigDecimal; +import java.util.*; + +import org.hl7.fhir.dstu3.context.IWorkerContext; +import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.ElementDefinition.TypeRefComponent; +import org.hl7.fhir.dstu3.model.ExpressionNode.*; +import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionKind; +import org.hl7.fhir.dstu3.model.StructureDefinition.TypeDerivationRule; +import org.hl7.fhir.dstu3.model.TypeDetails.ProfiledType; +import org.hl7.fhir.dstu3.utils.FHIRLexer.FHIRLexerException; +import org.hl7.fhir.dstu3.utils.FHIRPathEngine.IEvaluationContext.FunctionDetails; +import org.hl7.fhir.exceptions.*; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.ucum.Decimal; + +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.ElementUtil; + +/** + * + * @author Grahame Grieve + * + */ +public class FHIRPathEngine { + 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 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) + */ + public FHIRPathEngine(IWorkerContext worker) { + super(); + this.worker = worker; + for (StructureDefinition sd : worker.allStructures()) { + if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) + allTypes.put(sd.getName(), sd); + if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) { + primitiveTypes.add(sd.getName()); + } + } + } + + + // --- 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 + * + * @param path + * @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 + * @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('.')); + if (Utilities.isAbsoluteUrl(resourceType)) { + 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()); + } + } + + 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.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) + 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 { + types = new TypeDetails(CollectionStatus.SINGLETON); + for (TypeRefComponent t : ed.getDefinition().getType()) + types.addType(t.getCode()); + } + } + + return executeType(new ExecutionTypeContext(appContext, sd.getUrl(), context, types), types, expr, true); + } + + 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 + 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 TypeDetails check(Object appContext, String resourceType, String context, String expr) throws FHIRLexerException, PathEngineException, DefinitionException { + return check(appContext, resourceType, context, parse(expr)); + } + + + /** + * 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); + } + + /** + * 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 + * @param path - the FHIR Path statement to use + * @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 + * @param path - the FHIR Path statement to use + * @return + * @throws FHIRException + * @ + */ + public boolean evaluateToBoolean(Object appInfo, Resource 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 + * @param path - the FHIR Path statement to use + * @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)); + } + + /** + * 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; + else + log.append(","); + log.append(convertToString(b)); + } + } + } + + 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(); + } + 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 ExpressionNode gatherPrecedence(FHIRLexer lexer, ExpressionNode start, EnumSet ops) { + // work : boolean; + // focus, node, group : ExpressionNode; + + assert(start.isProximal()); + + // is there anything to do? + boolean work = false; + ExpressionNode focus = start.getOpNext(); + if (ops.contains(start.getOperation())) { + while (focus != null && focus.getOperation() != null) { + work = work || !ops.contains(focus.getOperation()); + focus = focus.getOpNext(); + } + } else { + while (focus != null && focus.getOperation() != null) { + work = work || ops.contains(focus.getOperation()); + focus = focus.getOpNext(); + } + } + if (!work) + return start; + + // entry point: tricky + ExpressionNode group; + if (ops.contains(start.getOperation())) { + group = newGroup(lexer, start); + group.setProximal(true); + focus = start; + start = group; + } else { + ExpressionNode node = start; + + focus = node.getOpNext(); + while (!ops.contains(focus.getOperation())) { + node = focus; + focus = focus.getOpNext(); + } + group = newGroup(lexer, focus); + node.setOpNext(group); + } + + // now, at this point: + // group is the group we are adding to, it already has a .group property filled out. + // focus points at the group.group + do { + // run until we find the end of the sequence + while (ops.contains(focus.getOperation())) + focus = focus.getOpNext(); + if (focus.getOperation() != null) { + group.setOperation(focus.getOperation()); + group.setOpNext(focus.getOpNext()); + focus.setOperation(null); + focus.setOpNext(null); + // now look for another sequence, and start it + ExpressionNode node = group; + focus = group.getOpNext(); + 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 + group = newGroup(lexer, focus); + node.setOpNext(group); + } + } + } + } + while (focus != null && focus.getOperation() != null); + return start; + } + + + 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 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++; + } + } + } + + // 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 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 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); + } + + 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 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) { + List res = new ArrayList(); + res.add(new BooleanType(b)); + 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); + } + 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); + 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()); + } + } + + private List opAs(List left, List right) { + List result = new ArrayList(); + if (left.size() != 1 || right.size() != 1) + return result; + else { + String tn = convertToString(right); + if (tn.equals(left.get(0).fhirType())) + result.add(left.get(0)); + } + return result; + } + + + 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 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 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.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 Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); + 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); + + 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(l.primitiveValue().compareTo(r.primitiveValue()) < 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(l.primitiveValue().compareTo(r.primitiveValue()) > 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(l.primitiveValue().compareTo(r.primitiveValue()) <= 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(l.primitiveValue().compareTo(r.primitiveValue()) >= 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 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 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()) + 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()).multiply(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 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())); + 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 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 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"); + else if (constant.equals("false")) + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + else if (Utilities.isInteger(constant)) + return new TypeDetails(CollectionStatus.SINGLETON, "integer"); + else if (Utilities.isDecimal(constant)) + return new TypeDetails(CollectionStatus.SINGLETON, "decimal"); + else if (constant.startsWith("%")) + return resolveConstantType(context, constant); + else + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + } + + private TypeDetails resolveConstantType(ExecutionTypeContext context, String s) throws PathEngineException { + if (s.equals("%sct")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.equals("%loinc")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.equals("%ucum")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.equals("%resource")) { + if (context.resource == null) + throw new PathEngineException("%resource cannot be used in this context"); + return new TypeDetails(CollectionStatus.SINGLETON, context.resource); + } else if (s.equals("%context")) { + return new TypeDetails(CollectionStatus.SINGLETON, context.context); + } else if (s.equals("%map-codes")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.equals("%us-zip")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.startsWith("%\"vs-")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.startsWith("%\"cs-")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.startsWith("%\"ext-")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (hostServices == null) + 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); + } + + + @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 ""; + String s = log.toString(); + log = new StringBuilder(); + return s; + } + +} diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/JsonTrackingParser.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/JsonTrackingParser.java index 94cbe94c1dd..8134327a701 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/JsonTrackingParser.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/JsonTrackingParser.java @@ -1,494 +1,501 @@ -package org.hl7.fhir.dstu3.utils; - -import java.math.BigDecimal; -import java.util.Map; -import java.util.Stack; - -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.utilities.Utilities; - -import com.google.gson.*; - - -/** - * This is created to get a json parser that can track line numbers... grr... - * - * @author Grahame Grieve - * - */ -public class JsonTrackingParser { - - public enum TokenType { - Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean; - } - - public class LocationData { - private int line; - private int col; - - protected LocationData(int line, int col) { - super(); - this.line = line; - this.col = col; - } - - public int getLine() { - return line; - } - - public int getCol() { - return col; - } - - public void newLine() { - line++; - col = 1; - } - - public LocationData copy() { - return new LocationData(line, col); - } - } - - private class State { - private String name; - private boolean isProp; - protected State(String name, boolean isProp) { - super(); - this.name = name; - this.isProp = isProp; - } - public String getName() { - return name; - } - public boolean isProp() { - return isProp; - } - } - - private class Lexer { - private String source; - private int cursor; - private String peek; - private String value; - private TokenType type; - private Stack states = new Stack(); - private LocationData lastLocationBWS; - private LocationData lastLocationAWS; - private LocationData location; - private StringBuilder b = new StringBuilder(); - - public Lexer(String source) throws FHIRException { - this.source = source; - cursor = -1; - location = new LocationData(1, 1); - start(); - } - - private boolean more() { - return peek != null || cursor < source.length(); - } - - private String getNext(int length) throws FHIRException { - String result = ""; - if (peek != null) { - if (peek.length() > length) { - result = peek.substring(0, length); - peek = peek.substring(length); - } else { - result = peek; - peek = null; - } - } - if (result.length() < length) { - int len = length - result.length(); - if (cursor > source.length() - len) - throw error("Attempt to read past end of source"); - result = result + source.substring(cursor+1, cursor+len+1); - cursor = cursor + len; - } - for (char ch : result.toCharArray()) - if (ch == '\n') - location.newLine(); - else - location.col++; - return result; - } - - private char getNextChar() throws FHIRException { - if (peek != null) { - char ch = peek.charAt(0); - peek = peek.length() == 1 ? null : peek.substring(1); - return ch; - } else { - cursor++; - if (cursor >= source.length()) - return (char) 0; - char ch = source.charAt(cursor); - if (ch == '\n') { - location.newLine(); - } else { - location.col++; - } - return ch; - } - } - - private void push(char ch){ - peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek; - } - - private void parseWord(String word, char ch, TokenType type) throws FHIRException { - this.type = type; - value = ""+ch+getNext(word.length()-1); - if (!value.equals(word)) - throw error("Syntax error in json reading special word "+word); - } - - private FHIRException error(String msg) { - return new FHIRException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.line)+" (path=["+path()+"])"); - } - - private String path() { - if (states.empty()) - return value; - else { - String result = ""; - for (State s : states) - result = result + '/'+ s.getName(); - result = result + value; - return result; - } - } - - public void start() throws FHIRException { -// char ch = getNextChar(); -// if (ch = '\.uEF') -// begin -// // skip BOM -// getNextChar(); -// getNextChar(); -// end -// else -// push(ch); - next(); - } - - public TokenType getType() { - return type; - } - - public String getValue() { - return value; - } - - - public LocationData getLastLocationBWS() { - return lastLocationBWS; - } - - public LocationData getLastLocationAWS() { - return lastLocationAWS; - } - - public void next() throws FHIRException { - lastLocationBWS = location.copy(); - char ch; - do { - ch = getNextChar(); - } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t')); - lastLocationAWS = location.copy(); - - if (!more()) { - type = TokenType.Eof; - } else { - switch (ch) { - case '{' : - type = TokenType.Open; - break; - case '}' : - type = TokenType.Close; - break; - case '"' : - type = TokenType.String; - b.setLength(0); - do { - ch = getNextChar(); - if (ch == '\\') { - ch = getNextChar(); - switch (ch) { - case '"': b.append('"'); break; - case '\\': b.append('\\'); break; - case '/': b.append('/'); break; - case 'n': b.append('\n'); break; - case 'r': b.append('\r'); break; - case 't': b.append('\t'); break; - case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break; - default : - throw error("unknown escape sequence: \\"+ch); - } - ch = ' '; - } else if (ch != '"') - b.append(ch); - } while (more() && (ch != '"')); - if (!more()) - throw error("premature termination of json stream during a string"); - value = b.toString(); - break; - case ':' : - type = TokenType.Colon; - break; - case ',' : - type = TokenType.Comma; - break; - case '[' : - type = TokenType.OpenArray; - break; - case ']' : - type = TokenType.CloseArray; - break; - case 't' : - parseWord("true", ch, TokenType.Boolean); - break; - case 'f' : - parseWord("false", ch, TokenType.Boolean); - break; - case 'n' : - parseWord("null", ch, TokenType.Null); - break; - default: - if ((ch >= '0' && ch <= '9') || ch == '-') { - type = TokenType.Number; - b.setLength(0); - while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.')) { - b.append(ch); - ch = getNextChar(); - } - value = b.toString(); - push(ch); - } else - throw error("Unexpected char '"+ch+"' in json stream"); - } - } - } - - public String consume(TokenType type) throws FHIRException { - if (this.type != type) - throw error("JSON syntax error - found "+type.toString()+" expecting "+type.toString()); - String result = value; - next(); - return result; - } - - } - - enum ItemType { - Object, String, Number, Boolean, Array, End, Eof, Null; - } - private Map map; - private Lexer lexer; - private ItemType itemType = ItemType.Object; - private String itemName; - private String itemValue; - - public static JsonObject parse(String source, Map map) throws FHIRException { - JsonTrackingParser self = new JsonTrackingParser(); - self.map = map; - return self.parse(source); - } - - private JsonObject parse(String source) throws FHIRException { - lexer = new Lexer(source); - JsonObject result = new JsonObject(); - LocationData loc = lexer.location.copy(); - if (lexer.getType() == TokenType.Open) { - lexer.next(); - lexer.states.push(new State("", false)); - } - else - throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString()); - - parseProperty(); - readObject(result, true); - map.put(result, loc); - return result; - } - - private void readObject(JsonObject obj, boolean root) throws FHIRException { - map.put(obj, lexer.location.copy()); - - while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) { - if (obj.has(itemName)) - throw lexer.error("Duplicated property name: "+itemName); - - switch (itemType) { - case Object: - JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName); - LocationData loc = lexer.location.copy(); - obj.add(itemName, child); - next(); - readObject(child, false); - map.put(obj, loc); - break; - case Boolean : - JsonPrimitive v = new JsonPrimitive(Boolean.valueOf(itemValue)); - obj.add(itemName, v); - map.put(v, lexer.location.copy()); - break; - case String: - v = new JsonPrimitive(itemValue); - obj.add(itemName, v); - map.put(v, lexer.location.copy()); - break; - case Number: - v = new JsonPrimitive(new BigDecimal(itemValue)); - obj.add(itemName, v); - map.put(v, lexer.location.copy()); - break; - case Null: - JsonNull n = new JsonNull(); - obj.add(itemName, n); - map.put(n, lexer.location.copy()); - break; - case Array: - JsonArray arr = new JsonArray(); // (obj.path+'.'+ItemName); - loc = lexer.location.copy(); - obj.add(itemName, arr); - next(); - readArray(arr, false); - map.put(arr, loc); - break; - case Eof : - throw lexer.error("Unexpected End of File"); - } - next(); - } - } - - private void readArray(JsonArray arr, boolean root) throws FHIRException { - while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) { - switch (itemType) { - case Object: - JsonObject obj = new JsonObject(); // (arr.path+'['+inttostr(i)+']'); - LocationData loc = lexer.location.copy(); - arr.add(obj); - next(); - readObject(obj, false); - map.put(obj, loc); - break; - case String: - JsonPrimitive v = new JsonPrimitive(itemValue); - arr.add(v); - map.put(v, lexer.location.copy()); - break; - case Number: - v = new JsonPrimitive(new BigDecimal(itemValue)); - arr.add(v); - map.put(v, lexer.location.copy()); - break; - case Null : - JsonNull n = new JsonNull(); - arr.add(n); - map.put(n, lexer.location.copy()); - break; - case Array: - JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']'); - loc = lexer.location.copy(); - arr.add(child); - next(); - readArray(child, false); - map.put(arr, loc); - break; - case Eof : - throw lexer.error("Unexpected End of File"); - } - next(); - } - } - - private void next() throws FHIRException { - switch (itemType) { - case Object : - lexer.consume(TokenType.Open); - lexer.states.push(new State(itemName, false)); - if (lexer.getType() == TokenType.Close) { - itemType = ItemType.End; - lexer.next(); - } else - parseProperty(); - break; - case Null: - case String: - case Number: - case End: - case Boolean : - if (itemType == ItemType.End) - lexer.states.pop(); - if (lexer.getType() == TokenType.Comma) { - lexer.next(); - parseProperty(); - } else if (lexer.getType() == TokenType.Close) { - itemType = ItemType.End; - lexer.next(); - } else if (lexer.getType() == TokenType.CloseArray) { - itemType = ItemType.End; - lexer.next(); - } else if (lexer.getType() == TokenType.Eof) { - itemType = ItemType.Eof; - } else - throw lexer.error("Unexpected JSON syntax"); - break; - case Array : - lexer.next(); - lexer.states.push(new State(itemName+"[]", true)); - parseProperty(); - break; - case Eof : - throw lexer.error("JSON Syntax Error - attempt to read past end of json stream"); - default: - throw lexer.error("not done yet (a): "+itemType.toString()); - } - } - - private void parseProperty() throws FHIRException { - if (!lexer.states.peek().isProp) { - itemName = lexer.consume(TokenType.String); - itemValue = null; - lexer.consume(TokenType.Colon); - } - switch (lexer.getType()) { - case Null : - itemType = ItemType.Null; - itemValue = lexer.value; - lexer.next(); - break; - case String : - itemType = ItemType.String; - itemValue = lexer.value; - lexer.next(); - break; - case Boolean : - itemType = ItemType.Boolean; - itemValue = lexer.value; - lexer.next(); - break; - case Number : - itemType = ItemType.Number; - itemValue = lexer.value; - lexer.next(); - break; - case Open : - itemType = ItemType.Object; - break; - case OpenArray : - itemType = ItemType.Array; - break; - case CloseArray : - itemType = ItemType.End; - break; - // case Close, , case Colon, case Comma, case OpenArray, ! - default: - throw lexer.error("not done yet (b): "+lexer.getType().toString()); - } - } -} +package org.hl7.fhir.dstu3.utils; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.Stack; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.utilities.Utilities; + +import com.google.gson.*; + + +/** + * This is created to get a json parser that can track line numbers... grr... + * + * @author Grahame Grieve + * + */ +public class JsonTrackingParser { + + public enum TokenType { + Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean; + } + + public class LocationData { + private int line; + private int col; + + protected LocationData(int line, int col) { + super(); + this.line = line; + this.col = col; + } + + public int getLine() { + return line; + } + + public int getCol() { + return col; + } + + public void newLine() { + line++; + col = 1; + } + + public LocationData copy() { + return new LocationData(line, col); + } + } + + private class State { + private String name; + private boolean isProp; + protected State(String name, boolean isProp) { + super(); + this.name = name; + this.isProp = isProp; + } + public String getName() { + return name; + } + public boolean isProp() { + return isProp; + } + } + + private class Lexer { + private String source; + private int cursor; + private String peek; + private String value; + private TokenType type; + private Stack states = new Stack(); + private LocationData lastLocationBWS; + private LocationData lastLocationAWS; + private LocationData location; + private StringBuilder b = new StringBuilder(); + + public Lexer(String source) throws FHIRException { + this.source = source; + cursor = -1; + location = new LocationData(1, 1); + start(); + } + + private boolean more() { + return peek != null || cursor < source.length(); + } + + private String getNext(int length) throws FHIRException { + String result = ""; + if (peek != null) { + if (peek.length() > length) { + result = peek.substring(0, length); + peek = peek.substring(length); + } else { + result = peek; + peek = null; + } + } + if (result.length() < length) { + int len = length - result.length(); + if (cursor > source.length() - len) + throw error("Attempt to read past end of source"); + result = result + source.substring(cursor+1, cursor+len+1); + cursor = cursor + len; + } + for (char ch : result.toCharArray()) + if (ch == '\n') + location.newLine(); + else + location.col++; + return result; + } + + private char getNextChar() throws FHIRException { + if (peek != null) { + char ch = peek.charAt(0); + peek = peek.length() == 1 ? null : peek.substring(1); + return ch; + } else { + cursor++; + if (cursor >= source.length()) + return (char) 0; + char ch = source.charAt(cursor); + if (ch == '\n') { + location.newLine(); + } else { + location.col++; + } + return ch; + } + } + + private void push(char ch){ + peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek; + } + + private void parseWord(String word, char ch, TokenType type) throws FHIRException { + this.type = type; + value = ""+ch+getNext(word.length()-1); + if (!value.equals(word)) + throw error("Syntax error in json reading special word "+word); + } + + private FHIRException error(String msg) { + return new FHIRException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.line)+" (path=["+path()+"])"); + } + + private String path() { + if (states.empty()) + return value; + else { + String result = ""; + for (State s : states) + result = result + '/'+ s.getName(); + result = result + value; + return result; + } + } + + public void start() throws FHIRException { +// char ch = getNextChar(); +// if (ch = '\.uEF') +// begin +// // skip BOM +// getNextChar(); +// getNextChar(); +// end +// else +// push(ch); + next(); + } + + public TokenType getType() { + return type; + } + + public String getValue() { + return value; + } + + + public LocationData getLastLocationBWS() { + return lastLocationBWS; + } + + public LocationData getLastLocationAWS() { + return lastLocationAWS; + } + + public void next() throws FHIRException { + lastLocationBWS = location.copy(); + char ch; + do { + ch = getNextChar(); + } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t')); + lastLocationAWS = location.copy(); + + if (!more()) { + type = TokenType.Eof; + } else { + switch (ch) { + case '{' : + type = TokenType.Open; + break; + case '}' : + type = TokenType.Close; + break; + case '"' : + type = TokenType.String; + b.setLength(0); + do { + ch = getNextChar(); + if (ch == '\\') { + ch = getNextChar(); + switch (ch) { + case '"': b.append('"'); break; + case '\\': b.append('\\'); break; + case '/': b.append('/'); break; + case 'n': b.append('\n'); break; + case 'r': b.append('\r'); break; + case 't': b.append('\t'); break; + case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break; + default : + throw error("unknown escape sequence: \\"+ch); + } + ch = ' '; + } else if (ch != '"') + b.append(ch); + } while (more() && (ch != '"')); + if (!more()) + throw error("premature termination of json stream during a string"); + value = b.toString(); + break; + case ':' : + type = TokenType.Colon; + break; + case ',' : + type = TokenType.Comma; + break; + case '[' : + type = TokenType.OpenArray; + break; + case ']' : + type = TokenType.CloseArray; + break; + case 't' : + parseWord("true", ch, TokenType.Boolean); + break; + case 'f' : + parseWord("false", ch, TokenType.Boolean); + break; + case 'n' : + parseWord("null", ch, TokenType.Null); + break; + default: + if ((ch >= '0' && ch <= '9') || ch == '-') { + type = TokenType.Number; + b.setLength(0); + while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.')) { + b.append(ch); + ch = getNextChar(); + } + value = b.toString(); + push(ch); + } else + throw error("Unexpected char '"+ch+"' in json stream"); + } + } + } + + public String consume(TokenType type) throws FHIRException { + if (this.type != type) + throw error("JSON syntax error - found "+type.toString()+" expecting "+type.toString()); + String result = value; + next(); + return result; + } + + } + + enum ItemType { + Object, String, Number, Boolean, Array, End, Eof, Null; + } + private Map map; + private Lexer lexer; + private ItemType itemType = ItemType.Object; + private String itemName; + private String itemValue; + + public static JsonObject parse(String source, Map map) throws FHIRException { + JsonTrackingParser self = new JsonTrackingParser(); + self.map = map; + return self.parse(source); + } + + private JsonObject parse(String source) throws FHIRException { + lexer = new Lexer(source); + JsonObject result = new JsonObject(); + LocationData loc = lexer.location.copy(); + if (lexer.getType() == TokenType.Open) { + lexer.next(); + lexer.states.push(new State("", false)); + } + else + throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString()); + + parseProperty(); + readObject(result, true); + map.put(result, loc); + return result; + } + + private void readObject(JsonObject obj, boolean root) throws FHIRException { + map.put(obj, lexer.location.copy()); + + while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) { + if (obj.has(itemName)) + throw lexer.error("Duplicated property name: "+itemName); + + switch (itemType) { + case Object: + JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName); + LocationData loc = lexer.location.copy(); + obj.add(itemName, child); + next(); + readObject(child, false); + map.put(obj, loc); + break; + case Boolean : + JsonPrimitive v = new JsonPrimitive(Boolean.valueOf(itemValue)); + obj.add(itemName, v); + map.put(v, lexer.location.copy()); + break; + case String: + v = new JsonPrimitive(itemValue); + obj.add(itemName, v); + map.put(v, lexer.location.copy()); + break; + case Number: + v = new JsonPrimitive(new BigDecimal(itemValue)); + obj.add(itemName, v); + map.put(v, lexer.location.copy()); + break; + case Null: + JsonNull n = new JsonNull(); + obj.add(itemName, n); + map.put(n, lexer.location.copy()); + break; + case Array: + JsonArray arr = new JsonArray(); // (obj.path+'.'+ItemName); + loc = lexer.location.copy(); + obj.add(itemName, arr); + next(); + readArray(arr, false); + map.put(arr, loc); + break; + case Eof : + throw lexer.error("Unexpected End of File"); + case End: + // TODO: anything?\ + break; + } + next(); + } + } + + private void readArray(JsonArray arr, boolean root) throws FHIRException { + while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) { + switch (itemType) { + case Object: + JsonObject obj = new JsonObject(); // (arr.path+'['+inttostr(i)+']'); + LocationData loc = lexer.location.copy(); + arr.add(obj); + next(); + readObject(obj, false); + map.put(obj, loc); + break; + case String: + JsonPrimitive v = new JsonPrimitive(itemValue); + arr.add(v); + map.put(v, lexer.location.copy()); + break; + case Number: + v = new JsonPrimitive(new BigDecimal(itemValue)); + arr.add(v); + map.put(v, lexer.location.copy()); + break; + case Null : + JsonNull n = new JsonNull(); + arr.add(n); + map.put(n, lexer.location.copy()); + break; + case Array: + JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']'); + loc = lexer.location.copy(); + arr.add(child); + next(); + readArray(child, false); + map.put(arr, loc); + break; + case Eof : + throw lexer.error("Unexpected End of File"); + case End: + case Boolean: + // TODO: anything? + break; + } + next(); + } + } + + private void next() throws FHIRException { + switch (itemType) { + case Object : + lexer.consume(TokenType.Open); + lexer.states.push(new State(itemName, false)); + if (lexer.getType() == TokenType.Close) { + itemType = ItemType.End; + lexer.next(); + } else + parseProperty(); + break; + case Null: + case String: + case Number: + case End: + case Boolean : + if (itemType == ItemType.End) + lexer.states.pop(); + if (lexer.getType() == TokenType.Comma) { + lexer.next(); + parseProperty(); + } else if (lexer.getType() == TokenType.Close) { + itemType = ItemType.End; + lexer.next(); + } else if (lexer.getType() == TokenType.CloseArray) { + itemType = ItemType.End; + lexer.next(); + } else if (lexer.getType() == TokenType.Eof) { + itemType = ItemType.Eof; + } else + throw lexer.error("Unexpected JSON syntax"); + break; + case Array : + lexer.next(); + lexer.states.push(new State(itemName+"[]", true)); + parseProperty(); + break; + case Eof : + throw lexer.error("JSON Syntax Error - attempt to read past end of json stream"); + default: + throw lexer.error("not done yet (a): "+itemType.toString()); + } + } + + private void parseProperty() throws FHIRException { + if (!lexer.states.peek().isProp) { + itemName = lexer.consume(TokenType.String); + itemValue = null; + lexer.consume(TokenType.Colon); + } + switch (lexer.getType()) { + case Null : + itemType = ItemType.Null; + itemValue = lexer.value; + lexer.next(); + break; + case String : + itemType = ItemType.String; + itemValue = lexer.value; + lexer.next(); + break; + case Boolean : + itemType = ItemType.Boolean; + itemValue = lexer.value; + lexer.next(); + break; + case Number : + itemType = ItemType.Number; + itemValue = lexer.value; + lexer.next(); + break; + case Open : + itemType = ItemType.Object; + break; + case OpenArray : + itemType = ItemType.Array; + break; + case CloseArray : + itemType = ItemType.End; + break; + // case Close, , case Colon, case Comma, case OpenArray, ! + default: + throw lexer.error("not done yet (b): "+lexer.getType().toString()); + } + } +} diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/StructureMapUtilities.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/StructureMapUtilities.java index 7f3ca68c8ca..cdd491e1c0f 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/StructureMapUtilities.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/StructureMapUtilities.java @@ -1,2723 +1,2727 @@ -package org.hl7.fhir.dstu3.utils; - -// remember group resolution -// trace - account for which wasn't transformed in the source - -import java.io.IOException; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import org.hl7.fhir.dstu3.conformance.ProfileUtilities; -import org.hl7.fhir.dstu3.conformance.ProfileUtilities.ProfileKnowledgeProvider; -import org.hl7.fhir.dstu3.context.IWorkerContext; -import org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult; -import org.hl7.fhir.dstu3.elementmodel.Element; -import org.hl7.fhir.dstu3.elementmodel.Property; -import org.hl7.fhir.dstu3.model.Base; -import org.hl7.fhir.dstu3.model.BooleanType; -import org.hl7.fhir.dstu3.model.CodeType; -import org.hl7.fhir.dstu3.model.CodeableConcept; -import org.hl7.fhir.dstu3.model.Coding; -import org.hl7.fhir.dstu3.model.ConceptMap; -import org.hl7.fhir.dstu3.model.ConceptMap.ConceptMapGroupComponent; -import org.hl7.fhir.dstu3.model.ConceptMap.SourceElementComponent; -import org.hl7.fhir.dstu3.model.ConceptMap.TargetElementComponent; -import org.hl7.fhir.dstu3.model.Constants; -import org.hl7.fhir.dstu3.model.ContactDetail; -import org.hl7.fhir.dstu3.model.ContactPoint; -import org.hl7.fhir.dstu3.model.DecimalType; -import org.hl7.fhir.dstu3.model.ElementDefinition; -import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionMappingComponent; -import org.hl7.fhir.dstu3.model.ElementDefinition.TypeRefComponent; -import org.hl7.fhir.dstu3.model.Enumeration; -import org.hl7.fhir.dstu3.model.Enumerations.ConceptMapEquivalence; -import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus; -import org.hl7.fhir.dstu3.model.ExpressionNode; -import org.hl7.fhir.dstu3.model.ExpressionNode.CollectionStatus; -import org.hl7.fhir.dstu3.model.IdType; -import org.hl7.fhir.dstu3.model.IntegerType; -import org.hl7.fhir.dstu3.model.Narrative.NarrativeStatus; -import org.hl7.fhir.dstu3.model.PrimitiveType; -import org.hl7.fhir.dstu3.model.Reference; -import org.hl7.fhir.dstu3.model.Resource; -import org.hl7.fhir.dstu3.model.ResourceFactory; -import org.hl7.fhir.dstu3.model.StringType; -import org.hl7.fhir.dstu3.model.StructureDefinition; -import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionMappingComponent; -import org.hl7.fhir.dstu3.model.StructureDefinition.TypeDerivationRule; -import org.hl7.fhir.dstu3.model.StructureMap; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapContextType; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupComponent; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupInputComponent; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleComponent; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleDependentComponent; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleSourceComponent; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleTargetComponent; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleTargetParameterComponent; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupTypeMode; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapInputMode; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapSourceListMode; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapTargetListMode; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapModelMode; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapStructureComponent; -import org.hl7.fhir.dstu3.model.StructureMap.StructureMapTransform; -import org.hl7.fhir.dstu3.model.Type; -import org.hl7.fhir.dstu3.model.TypeDetails; -import org.hl7.fhir.dstu3.model.TypeDetails.ProfiledType; -import org.hl7.fhir.dstu3.model.UriType; -import org.hl7.fhir.dstu3.model.ValueSet; -import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent; -import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.ValueSetExpansionOutcome; -import org.hl7.fhir.dstu3.utils.FHIRLexer.FHIRLexerException; -import org.hl7.fhir.dstu3.utils.FHIRPathEngine.IEvaluationContext; -import org.hl7.fhir.exceptions.DefinitionException; -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.exceptions.PathEngineException; -import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; -import org.hl7.fhir.utilities.TextFile; -import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.xhtml.NodeType; -import org.hl7.fhir.utilities.xhtml.XhtmlNode; - -/** - * Services in this class: - * - * string render(map) - take a structure and convert it to text - * map parse(text) - take a text representation and parse it - * getTargetType(map) - return the definition for the type to create to hand in - * transform(appInfo, source, map, target) - transform from source to target following the map - * analyse(appInfo, map) - generate profiles and other analysis artifacts for the targets of the transform - * map generateMapFromMappings(StructureDefinition) - build a mapping from a structure definition with loigcal mappings - * - * @author Grahame Grieve - * - */ -public class StructureMapUtilities { - - public class ResolvedGroup { - public StructureMapGroupComponent target; - public StructureMap targetMap; - } - public static final String MAP_WHERE_CHECK = "map.where.check"; - public static final String MAP_WHERE_EXPRESSION = "map.where.expression"; - public static final String MAP_SEARCH_EXPRESSION = "map.search.expression"; - public static final String MAP_EXPRESSION = "map.transform.expression"; - private static final boolean RENDER_MULTIPLE_TARGETS_ONELINE = true; - private static final String AUTO_VAR_NAME = "vvv"; - - public interface ITransformerServices { - // public boolean validateByValueSet(Coding code, String valuesetId); - public void log(String message); // log internal progress - public Base createType(Object appInfo, String name) throws FHIRException; - public Base createResource(Object appInfo, Base res); // an already created resource is provided; this is to identify/store it - public Coding translate(Object appInfo, Coding source, String conceptMapUrl) throws FHIRException; - // public Coding translate(Coding code) - // ValueSet validation operation - // Translation operation - // Lookup another tree of data - // Create an instance tree - // Return the correct string format to refer to a tree (input or output) - public Base resolveReference(Object appContext, String url); - public List performSearch(Object appContext, String url); - } - - private class FFHIRPathHostServices implements IEvaluationContext{ - - public Base resolveConstant(Object appContext, String name) throws PathEngineException { - Variables vars = (Variables) appContext; - Base res = vars.get(VariableMode.INPUT, name); - if (res == null) - res = vars.get(VariableMode.OUTPUT, name); - return res; - } - - @Override - public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { - if (!(appContext instanceof VariablesForProfiling)) - throw new Error("Internal Logic Error (wrong type '"+appContext.getClass().getName()+"' in resolveConstantType)"); - VariablesForProfiling vars = (VariablesForProfiling) appContext; - VariableForProfiling v = vars.get(null, name); - if (v == null) - throw new PathEngineException("Unknown variable '"+name+"' from variables "+vars.summary()); - return v.property.types; - } - - @Override - public boolean log(String argument, List focus) { - throw new Error("Not Implemented Yet"); - } - - @Override - public FunctionDetails resolveFunction(String functionName) { - return null; // throw new Error("Not Implemented Yet"); - } - - @Override - public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { - throw new Error("Not Implemented Yet"); - } - - @Override - public List executeFunction(Object appContext, String functionName, List> parameters) { - throw new Error("Not Implemented Yet"); - } - - @Override - public Base resolveReference(Object appContext, String url) { - if (services == null) - return null; - return services.resolveReference(appContext, url); - } - - } - private IWorkerContext worker; - private FHIRPathEngine fpe; - private Map library; - private ITransformerServices services; - private ProfileKnowledgeProvider pkp; - private Map ids = new HashMap(); - - public StructureMapUtilities(IWorkerContext worker, Map library, ITransformerServices services, ProfileKnowledgeProvider pkp) { - super(); - this.worker = worker; - this.library = library; - this.services = services; - this.pkp = pkp; - fpe = new FHIRPathEngine(worker); - fpe.setHostServices(new FFHIRPathHostServices()); - } - - public StructureMapUtilities(IWorkerContext worker, Map library, ITransformerServices services) { - super(); - this.worker = worker; - this.library = library; - this.services = services; - fpe = new FHIRPathEngine(worker); - fpe.setHostServices(new FFHIRPathHostServices()); - } - - public StructureMapUtilities(IWorkerContext worker, Map library) { - super(); - this.worker = worker; - this.library = library; - fpe = new FHIRPathEngine(worker); - fpe.setHostServices(new FFHIRPathHostServices()); - } - - public StructureMapUtilities(IWorkerContext worker) { - super(); - this.worker = worker; - fpe = new FHIRPathEngine(worker); - fpe.setHostServices(new FFHIRPathHostServices()); - } - - public StructureMapUtilities(IWorkerContext worker, ITransformerServices services) { - super(); - this.worker = worker; - this.library = new HashMap(); - for (org.hl7.fhir.dstu3.model.MetadataResource bc : worker.allConformanceResources()) { - if (bc instanceof StructureMap) - library.put(bc.getUrl(), (StructureMap) bc); - } - this.services = services; - fpe = new FHIRPathEngine(worker); - fpe.setHostServices(new FFHIRPathHostServices()); - } - - public static String render(StructureMap map) { - StringBuilder b = new StringBuilder(); - b.append("map \""); - b.append(map.getUrl()); - b.append("\" = \""); - b.append(Utilities.escapeJava(map.getName())); - b.append("\"\r\n\r\n"); - - renderConceptMaps(b, map); - renderUses(b, map); - renderImports(b, map); - for (StructureMapGroupComponent g : map.getGroup()) - renderGroup(b, g); - return b.toString(); - } - - private static void renderConceptMaps(StringBuilder b, StructureMap map) { - for (Resource r : map.getContained()) { - if (r instanceof ConceptMap) { - produceConceptMap(b, (ConceptMap) r); - } - } - } - - private static void produceConceptMap(StringBuilder b, ConceptMap cm) { - b.append("conceptmap \""); - b.append(cm.getId()); - b.append("\" {\r\n"); - Map prefixesSrc = new HashMap(); - Map prefixesTgt = new HashMap(); - char prefix = 's'; - for (ConceptMapGroupComponent cg : cm.getGroup()) { - if (!prefixesSrc.containsKey(cg.getSource())) { - prefixesSrc.put(cg.getSource(), String.valueOf(prefix)); - b.append(" prefix "); - b.append(prefix); - b.append(" = \""); - b.append(cg.getSource()); - b.append("\"\r\n"); - prefix++; - } - if (!prefixesTgt.containsKey(cg.getTarget())) { - prefixesTgt.put(cg.getTarget(), String.valueOf(prefix)); - b.append(" prefix "); - b.append(prefix); - b.append(" = \""); - b.append(cg.getTarget()); - b.append("\"\r\n"); - prefix++; - } - } - b.append("\r\n"); - for (ConceptMapGroupComponent cg : cm.getGroup()) { - for (SourceElementComponent ce : cg.getElement()) { - b.append(" "); - b.append(prefixesSrc.get(cg.getSource())); - b.append(":"); - b.append(ce.getCode()); - b.append(" "); - b.append(getChar(ce.getTargetFirstRep().getEquivalence())); - b.append(" "); - b.append(prefixesTgt.get(cg.getTarget())); - b.append(":"); - b.append(ce.getTargetFirstRep().getCode()); - b.append("\r\n"); - } - } - b.append("}\r\n\r\n"); - } - - private static Object getChar(ConceptMapEquivalence equivalence) { - switch (equivalence) { - case RELATEDTO: return "-"; - case EQUAL: return "="; - case EQUIVALENT: return "=="; - case DISJOINT: return "!="; - case UNMATCHED: return "--"; - case WIDER: return "<="; - case SUBSUMES: return "<-"; - case NARROWER: return ">="; - case SPECIALIZES: return ">-"; - case INEXACT: return "~"; - default: return "??"; - } - } - - private static void renderUses(StringBuilder b, StructureMap map) { - for (StructureMapStructureComponent s : map.getStructure()) { - b.append("uses \""); - b.append(s.getUrl()); - b.append("\" "); - if (s.hasAlias()) { - b.append("alias "); - b.append(s.getAlias()); - b.append(" "); - } - b.append("as "); - b.append(s.getMode().toCode()); - b.append("\r\n"); - renderDoco(b, s.getDocumentation()); - } - if (map.hasStructure()) - b.append("\r\n"); - } - - private static void renderImports(StringBuilder b, StructureMap map) { - for (UriType s : map.getImport()) { - b.append("imports \""); - b.append(s.getValue()); - b.append("\"\r\n"); - } - if (map.hasImport()) - b.append("\r\n"); - } - - public static String groupToString(StructureMapGroupComponent g) { - StringBuilder b = new StringBuilder(); - renderGroup(b, g); - return b.toString(); - } - - private static void renderGroup(StringBuilder b, StructureMapGroupComponent g) { - b.append("group "); - switch (g.getTypeMode()) { - case TYPES: b.append("for types"); - case TYPEANDTYPES: b.append("for type+types "); - default: // NONE, NULL - } - b.append("for types "); - b.append(g.getName()); - if (g.hasExtends()) { - b.append(" extends "); - b.append(g.getExtends()); - } - if (g.hasDocumentation()) - renderDoco(b, g.getDocumentation()); - b.append("\r\n"); - for (StructureMapGroupInputComponent gi : g.getInput()) { - b.append(" input "); - b.append(gi.getName()); - if (gi.hasType()) { - b.append(" : "); - b.append(gi.getType()); - } - b.append(" as "); - b.append(gi.getMode().toCode()); - b.append("\r\n"); - } - if (g.hasInput()) - b.append("\r\n"); - for (StructureMapGroupRuleComponent r : g.getRule()) { - renderRule(b, r, 2); - } - b.append("\r\nendgroup\r\n"); - } - - public static String ruleToString(StructureMapGroupRuleComponent r) { - StringBuilder b = new StringBuilder(); - renderRule(b, r, 0); - return b.toString(); - } - - private static void renderRule(StringBuilder b, StructureMapGroupRuleComponent r, int indent) { - for (int i = 0; i < indent; i++) - b.append(' '); - b.append(r.getName()); - b.append(" : for "); - boolean canBeAbbreviated = checkisSimple(r); - - boolean first = true; - for (StructureMapGroupRuleSourceComponent rs : r.getSource()) { - if (first) - first = false; - else - b.append(", "); - renderSource(b, rs, canBeAbbreviated); - } - if (r.getTarget().size() > 1) { - b.append(" make "); - first = true; - for (StructureMapGroupRuleTargetComponent rt : r.getTarget()) { - if (first) - first = false; - else - b.append(", "); - if (RENDER_MULTIPLE_TARGETS_ONELINE) - b.append(' '); - else { - b.append("\r\n"); - for (int i = 0; i < indent+4; i++) - b.append(' '); - } - renderTarget(b, rt, false); - } - } else if (r.hasTarget()) { - b.append(" make "); - renderTarget(b, r.getTarget().get(0), canBeAbbreviated); - } - if (!canBeAbbreviated) { - if (r.hasRule()) { - b.append(" then {\r\n"); - renderDoco(b, r.getDocumentation()); - for (StructureMapGroupRuleComponent ir : r.getRule()) { - renderRule(b, ir, indent+2); - } - for (int i = 0; i < indent; i++) - b.append(' '); - b.append("}\r\n"); - } else { - if (r.hasDependent()) { - b.append(" then "); - first = true; - for (StructureMapGroupRuleDependentComponent rd : r.getDependent()) { - if (first) - first = false; - else - b.append(", "); - b.append(rd.getName()); - b.append("("); - boolean ifirst = true; - for (StringType rdp : rd.getVariable()) { - if (ifirst) - ifirst = false; - else - b.append(", "); - b.append(rdp.asStringValue()); - } - b.append(")"); - } - } - } - } - renderDoco(b, r.getDocumentation()); - b.append("\r\n"); - } - - private static boolean checkisSimple(StructureMapGroupRuleComponent r) { - return - (r.getSource().size() == 1 && r.getSourceFirstRep().hasElement() && r.getSourceFirstRep().hasVariable()) && - (r.getTarget().size() == 1 && r.getTargetFirstRep().hasVariable() && (r.getTargetFirstRep().getTransform() == null || r.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE) && r.getTargetFirstRep().getParameter().size() == 0) && - (r.getDependent().size() == 0); - } - - public static String sourceToString(StructureMapGroupRuleSourceComponent r) { - StringBuilder b = new StringBuilder(); - renderSource(b, r, false); - return b.toString(); - } - - private static void renderSource(StringBuilder b, StructureMapGroupRuleSourceComponent rs, boolean abbreviate) { - b.append(rs.getContext()); - if (rs.getContext().equals("@search")) { - b.append('('); - b.append(rs.getElement()); - b.append(')'); - } else if (rs.hasElement()) { - b.append('.'); - b.append(rs.getElement()); - } - if (rs.hasType()) { - b.append(" : "); - b.append(rs.getType()); - if (rs.hasMin()) { - b.append(" "); - b.append(rs.getMin()); - b.append(".."); - b.append(rs.getMax()); - } - } - - if (rs.hasListMode()) { - b.append(" "); - b.append(rs.getListMode().toCode()); - } - if (rs.hasDefaultValue()) { - b.append(" default "); - assert rs.getDefaultValue() instanceof StringType; - b.append("\""+Utilities.escapeJson(((StringType) rs.getDefaultValue()).asStringValue())+"\""); - } - if (!abbreviate && rs.hasVariable()) { - b.append(" as "); - b.append(rs.getVariable()); - } - if (rs.hasCondition()) { - b.append(" where "); - b.append(rs.getCondition()); - } - if (rs.hasCheck()) { - b.append(" check "); - b.append(rs.getCheck()); - } - } - - public static String targetToString(StructureMapGroupRuleTargetComponent rt) { - StringBuilder b = new StringBuilder(); - renderTarget(b, rt, false); - return b.toString(); - } - - private static void renderTarget(StringBuilder b, StructureMapGroupRuleTargetComponent rt, boolean abbreviate) { - if (rt.hasContext()) { - if (rt.getContextType() == StructureMapContextType.TYPE) - b.append("@"); - b.append(rt.getContext()); - if (rt.hasElement()) { - b.append('.'); - b.append(rt.getElement()); - } - } - if (!abbreviate && rt.hasTransform()) { - if (rt.hasContext()) - b.append(" = "); - if (rt.getTransform() == StructureMapTransform.COPY && rt.getParameter().size() == 1) { - renderTransformParam(b, rt.getParameter().get(0)); - } else if (rt.getTransform() == StructureMapTransform.EVALUATE && rt.getParameter().size() == 1) { - b.append("("); - b.append("\""+((StringType) rt.getParameter().get(0).getValue()).asStringValue()+"\""); - b.append(")"); - } else if (rt.getTransform() == StructureMapTransform.EVALUATE && rt.getParameter().size() == 2) { - b.append(rt.getTransform().toCode()); - b.append("("); - b.append(((IdType) rt.getParameter().get(0).getValue()).asStringValue()); - b.append("\""+((StringType) rt.getParameter().get(1).getValue()).asStringValue()+"\""); - b.append(")"); - } else { - b.append(rt.getTransform().toCode()); - b.append("("); - boolean first = true; - for (StructureMapGroupRuleTargetParameterComponent rtp : rt.getParameter()) { - if (first) - first = false; - else - b.append(", "); - renderTransformParam(b, rtp); - } - b.append(")"); - } - } - if (!abbreviate && rt.hasVariable()) { - b.append(" as "); - b.append(rt.getVariable()); - } - for (Enumeration lm : rt.getListMode()) { - b.append(" "); - b.append(lm.getValue().toCode()); - if (lm.getValue() == StructureMapTargetListMode.SHARE) { - b.append(" "); - b.append(rt.getListRuleId()); - } - } - } - - public static String paramToString(StructureMapGroupRuleTargetParameterComponent rtp) { - StringBuilder b = new StringBuilder(); - renderTransformParam(b, rtp); - return b.toString(); - } - - private static void renderTransformParam(StringBuilder b, StructureMapGroupRuleTargetParameterComponent rtp) { - try { - if (rtp.hasValueBooleanType()) - b.append(rtp.getValueBooleanType().asStringValue()); - else if (rtp.hasValueDecimalType()) - b.append(rtp.getValueDecimalType().asStringValue()); - else if (rtp.hasValueIdType()) - b.append(rtp.getValueIdType().asStringValue()); - else if (rtp.hasValueDecimalType()) - b.append(rtp.getValueDecimalType().asStringValue()); - else if (rtp.hasValueIntegerType()) - b.append(rtp.getValueIntegerType().asStringValue()); - else - b.append("\""+Utilities.escapeJava(rtp.getValueStringType().asStringValue())+"\""); - } catch (FHIRException e) { - e.printStackTrace(); - b.append("error!"); - } - } - - private static void renderDoco(StringBuilder b, String doco) { - if (Utilities.noString(doco)) - return; - b.append(" // "); - b.append(doco.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")); - } - - public StructureMap parse(String text) throws FHIRException { - FHIRLexer lexer = new FHIRLexer(text); - if (lexer.done()) - throw lexer.error("Map Input cannot be empty"); - lexer.skipComments(); - lexer.token("map"); - StructureMap result = new StructureMap(); - result.setUrl(lexer.readConstant("url")); - lexer.token("="); - result.setName(lexer.readConstant("name")); - lexer.skipComments(); - - while (lexer.hasToken("conceptmap")) - parseConceptMap(result, lexer); - - while (lexer.hasToken("uses")) - parseUses(result, lexer); - while (lexer.hasToken("imports")) - parseImports(result, lexer); - - parseGroup(result, lexer); - - while (!lexer.done()) { - parseGroup(result, lexer); - } - - return result; - } - - private void parseConceptMap(StructureMap result, FHIRLexer lexer) throws FHIRLexerException { - lexer.token("conceptmap"); - ConceptMap map = new ConceptMap(); - String id = lexer.readConstant("map id"); - if (!id.startsWith("#")) - lexer.error("Concept Map identifier must start with #"); - map.setId(id); - map.setStatus(PublicationStatus.DRAFT); // todo: how to add this to the text format - result.getContained().add(map); - lexer.token("{"); - lexer.skipComments(); - // lexer.token("source"); - // map.setSource(new UriType(lexer.readConstant("source"))); - // lexer.token("target"); - // map.setSource(new UriType(lexer.readConstant("target"))); - Map prefixes = new HashMap(); - while (lexer.hasToken("prefix")) { - lexer.token("prefix"); - String n = lexer.take(); - lexer.token("="); - String v = lexer.readConstant("prefix url"); - prefixes.put(n, v); - } - while (!lexer.hasToken("}")) { - String srcs = readPrefix(prefixes, lexer); - lexer.token(":"); - String sc = lexer.getCurrent().startsWith("\"") ? lexer.readConstant("code") : lexer.take(); - ConceptMapEquivalence eq = readEquivalence(lexer); - String tgts = (eq != ConceptMapEquivalence.UNMATCHED) ? readPrefix(prefixes, lexer) : ""; - ConceptMapGroupComponent g = getGroup(map, srcs, tgts); - SourceElementComponent e = g.addElement(); - e.setCode(sc); - if (e.getCode().startsWith("\"")) - e.setCode(lexer.processConstant(e.getCode())); - TargetElementComponent tgt = e.addTarget(); - if (eq != ConceptMapEquivalence.EQUIVALENT) - tgt.setEquivalence(eq); - if (tgt.getEquivalence() != ConceptMapEquivalence.UNMATCHED) { - lexer.token(":"); - tgt.setCode(lexer.take()); - if (tgt.getCode().startsWith("\"")) - tgt.setCode(lexer.processConstant(tgt.getCode())); - } - if (lexer.hasComment()) - tgt.setComment(lexer.take().substring(2).trim()); - } - lexer.token("}"); - } - - - private ConceptMapGroupComponent getGroup(ConceptMap map, String srcs, String tgts) { - for (ConceptMapGroupComponent grp : map.getGroup()) { - if (grp.getSource().equals(srcs)) - if ((tgts == null && !grp.hasTarget()) || (tgts != null && tgts.equals(grp.getTarget()))) - return grp; - } - ConceptMapGroupComponent grp = map.addGroup(); - grp.setSource(srcs); - grp.setTarget(tgts); - return grp; - } - - - private String readPrefix(Map prefixes, FHIRLexer lexer) throws FHIRLexerException { - String prefix = lexer.take(); - if (!prefixes.containsKey(prefix)) - throw lexer.error("Unknown prefix '"+prefix+"'"); - return prefixes.get(prefix); - } - - - private ConceptMapEquivalence readEquivalence(FHIRLexer lexer) throws FHIRLexerException { - String token = lexer.take(); - if (token.equals("-")) - return ConceptMapEquivalence.RELATEDTO; - if (token.equals("=")) - return ConceptMapEquivalence.EQUAL; - if (token.equals("==")) - return ConceptMapEquivalence.EQUIVALENT; - if (token.equals("!=")) - return ConceptMapEquivalence.DISJOINT; - if (token.equals("--")) - return ConceptMapEquivalence.UNMATCHED; - if (token.equals("<=")) - return ConceptMapEquivalence.WIDER; - if (token.equals("<-")) - return ConceptMapEquivalence.SUBSUMES; - if (token.equals(">=")) - return ConceptMapEquivalence.NARROWER; - if (token.equals(">-")) - return ConceptMapEquivalence.SPECIALIZES; - if (token.equals("~")) - return ConceptMapEquivalence.INEXACT; - throw lexer.error("Unknown equivalence token '"+token+"'"); - } - - - private void parseUses(StructureMap result, FHIRLexer lexer) throws FHIRException { - lexer.token("uses"); - StructureMapStructureComponent st = result.addStructure(); - st.setUrl(lexer.readConstant("url")); - if (lexer.hasToken("alias")) { - lexer.token("alias"); - st.setAlias(lexer.take()); - } - lexer.token("as"); - st.setMode(StructureMapModelMode.fromCode(lexer.take())); - lexer.skipToken(";"); - if (lexer.hasComment()) { - st.setDocumentation(lexer.take().substring(2).trim()); - } - lexer.skipComments(); - } - - private void parseImports(StructureMap result, FHIRLexer lexer) throws FHIRException { - lexer.token("imports"); - result.addImport(lexer.readConstant("url")); - lexer.skipToken(";"); - if (lexer.hasComment()) { - lexer.next(); - } - lexer.skipComments(); - } - - private void parseGroup(StructureMap result, FHIRLexer lexer) throws FHIRException { - lexer.token("group"); - StructureMapGroupComponent group = result.addGroup(); - if (lexer.hasToken("for")) { - lexer.token("for"); - if ("type".equals(lexer.getCurrent())) { - lexer.token("type"); - lexer.token("+"); - lexer.token("types"); - group.setTypeMode(StructureMapGroupTypeMode.TYPEANDTYPES); - } else { - lexer.token("types"); - group.setTypeMode(StructureMapGroupTypeMode.TYPES); - } - } else - group.setTypeMode(StructureMapGroupTypeMode.NONE); - group.setName(lexer.take()); - if (lexer.hasToken("extends")) { - lexer.next(); - group.setExtends(lexer.take()); - } - lexer.skipComments(); - while (lexer.hasToken("input")) - parseInput(group, lexer); - while (!lexer.hasToken("endgroup")) { - if (lexer.done()) - throw lexer.error("premature termination expecting 'endgroup'"); - parseRule(result, group.getRule(), lexer); - } - lexer.next(); - lexer.skipComments(); - } - - private void parseInput(StructureMapGroupComponent group, FHIRLexer lexer) throws FHIRException { - lexer.token("input"); - StructureMapGroupInputComponent input = group.addInput(); - input.setName(lexer.take()); - if (lexer.hasToken(":")) { - lexer.token(":"); - input.setType(lexer.take()); - } - lexer.token("as"); - input.setMode(StructureMapInputMode.fromCode(lexer.take())); - if (lexer.hasComment()) { - input.setDocumentation(lexer.take().substring(2).trim()); - } - lexer.skipToken(";"); - lexer.skipComments(); - } - - private void parseRule(StructureMap map, List list, FHIRLexer lexer) throws FHIRException { - StructureMapGroupRuleComponent rule = new StructureMapGroupRuleComponent(); - list.add(rule); - rule.setName(lexer.takeDottedToken()); - lexer.token(":"); - lexer.token("for"); - boolean done = false; - while (!done) { - parseSource(rule, lexer); - done = !lexer.hasToken(","); - if (!done) - lexer.next(); - } - if (lexer.hasToken("make")) { - lexer.token("make"); - done = false; - while (!done) { - parseTarget(rule, lexer); - done = !lexer.hasToken(","); - if (!done) - lexer.next(); - } - } - if (lexer.hasToken("then")) { - lexer.token("then"); - if (lexer.hasToken("{")) { - lexer.token("{"); - if (lexer.hasComment()) { - rule.setDocumentation(lexer.take().substring(2).trim()); - } - lexer.skipComments(); - while (!lexer.hasToken("}")) { - if (lexer.done()) - throw lexer.error("premature termination expecting '}' in nested group"); - parseRule(map, rule.getRule(), lexer); - } - lexer.token("}"); - } else { - done = false; - while (!done) { - parseRuleReference(rule, lexer); - done = !lexer.hasToken(","); - if (!done) - lexer.next(); - } - } - } else if (lexer.hasComment()) { - rule.setDocumentation(lexer.take().substring(2).trim()); - } - if (isSimpleSyntax(rule)) { - rule.getSourceFirstRep().setVariable(AUTO_VAR_NAME); - rule.getTargetFirstRep().setVariable(AUTO_VAR_NAME); - rule.getTargetFirstRep().setTransform(StructureMapTransform.CREATE); // with no parameter - e.g. imply what is to be created - // no dependencies - imply what is to be done based on types - } - lexer.skipComments(); - } - - private boolean isSimpleSyntax(StructureMapGroupRuleComponent rule) { - return - (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasContext() && rule.getSourceFirstRep().hasElement() && !rule.getSourceFirstRep().hasVariable()) && - (rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasContext() && rule.getTargetFirstRep().hasElement() && !rule.getTargetFirstRep().hasVariable() && !rule.getTargetFirstRep().hasParameter()) && - (rule.getDependent().size() == 0 && rule.getRule().size() == 0); - } - - private void parseRuleReference(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRLexerException { - StructureMapGroupRuleDependentComponent ref = rule.addDependent(); - ref.setName(lexer.take()); - lexer.token("("); - boolean done = false; - while (!done) { - ref.addVariable(lexer.take()); - done = !lexer.hasToken(","); - if (!done) - lexer.next(); - } - lexer.token(")"); - } - - private void parseSource(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRException { - StructureMapGroupRuleSourceComponent source = rule.addSource(); - source.setContext(lexer.take()); - if (source.getContext().equals("search") && lexer.hasToken("(")) { - source.setContext("@search"); - lexer.take(); - ExpressionNode node = fpe.parse(lexer); - source.setUserData(MAP_SEARCH_EXPRESSION, node); - source.setElement(node.toString()); - lexer.token(")"); - } else if (lexer.hasToken(".")) { - lexer.token("."); - source.setElement(lexer.take()); - } - if (lexer.hasToken(":")) { - // type and cardinality - lexer.token(":"); - source.setType(lexer.takeDottedToken()); - if (!lexer.hasToken("as", "first", "last", "not_first", "not_last", "only_one", "default")) { - source.setMin(lexer.takeInt()); - lexer.token(".."); - source.setMax(lexer.take()); - } - } - if (lexer.hasToken("default")) { - lexer.token("default"); - source.setDefaultValue(new StringType(lexer.readConstant("default value"))); - } - if (Utilities.existsInList(lexer.getCurrent(), "first", "last", "not_first", "not_last", "only_one")) - source.setListMode(StructureMapSourceListMode.fromCode(lexer.take())); - - if (lexer.hasToken("as")) { - lexer.take(); - source.setVariable(lexer.take()); - } - if (lexer.hasToken("where")) { - lexer.take(); - ExpressionNode node = fpe.parse(lexer); - source.setUserData(MAP_WHERE_EXPRESSION, node); - source.setCondition(node.toString()); - } - if (lexer.hasToken("check")) { - lexer.take(); - ExpressionNode node = fpe.parse(lexer); - source.setUserData(MAP_WHERE_CHECK, node); - source.setCheck(node.toString()); - } - } - - private void parseTarget(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRException { - StructureMapGroupRuleTargetComponent target = rule.addTarget(); - String start = lexer.take(); - if (lexer.hasToken(".")) { - target.setContext(start); - target.setContextType(StructureMapContextType.VARIABLE); - start = null; - lexer.token("."); - target.setElement(lexer.take()); - } - String name; - boolean isConstant = false; - if (lexer.hasToken("=")) { - if (start != null) - target.setContext(start); - lexer.token("="); - isConstant = lexer.isConstant(true); - name = lexer.take(); - } else - name = start; - - if ("(".equals(name)) { - // inline fluentpath expression - target.setTransform(StructureMapTransform.EVALUATE); - ExpressionNode node = fpe.parse(lexer); - target.setUserData(MAP_EXPRESSION, node); - target.addParameter().setValue(new StringType(node.toString())); - lexer.token(")"); - } else if (lexer.hasToken("(")) { - target.setTransform(StructureMapTransform.fromCode(name)); - lexer.token("("); - if (target.getTransform() == StructureMapTransform.EVALUATE) { - parseParameter(target, lexer); - lexer.token(","); - ExpressionNode node = fpe.parse(lexer); - target.setUserData(MAP_EXPRESSION, node); - target.addParameter().setValue(new StringType(node.toString())); - } else { - while (!lexer.hasToken(")")) { - parseParameter(target, lexer); - if (!lexer.hasToken(")")) - lexer.token(","); - } - } - lexer.token(")"); - } else if (name != null) { - target.setTransform(StructureMapTransform.COPY); - if (!isConstant) { - String id = name; - while (lexer.hasToken(".")) { - id = id + lexer.take() + lexer.take(); - } - target.addParameter().setValue(new IdType(id)); - } - else - target.addParameter().setValue(readConstant(name, lexer)); - } - if (lexer.hasToken("as")) { - lexer.take(); - target.setVariable(lexer.take()); - } - while (Utilities.existsInList(lexer.getCurrent(), "first", "last", "share", "collate")) { - if (lexer.getCurrent().equals("share")) { - target.addListMode(StructureMapTargetListMode.SHARE); - lexer.next(); - target.setListRuleId(lexer.take()); - } else if (lexer.getCurrent().equals("first")) - target.addListMode(StructureMapTargetListMode.FIRST); - else - target.addListMode(StructureMapTargetListMode.LAST); - lexer.next(); - } - } - - - private void parseParameter(StructureMapGroupRuleTargetComponent target, FHIRLexer lexer) throws FHIRLexerException { - if (!lexer.isConstant(true)) { - target.addParameter().setValue(new IdType(lexer.take())); - } else if (lexer.isStringConstant()) - target.addParameter().setValue(new StringType(lexer.readConstant("??"))); - else { - target.addParameter().setValue(readConstant(lexer.take(), lexer)); - } - } - - private Type readConstant(String s, FHIRLexer lexer) throws FHIRLexerException { - if (Utilities.isInteger(s)) - return new IntegerType(s); - else if (Utilities.isDecimal(s)) - return new DecimalType(s); - else if (Utilities.existsInList(s, "true", "false")) - return new BooleanType(s.equals("true")); - else - return new StringType(lexer.processConstant(s)); - } - - public StructureDefinition getTargetType(StructureMap map) throws FHIRException { - boolean found = false; - StructureDefinition res = null; - for (StructureMapStructureComponent uses : map.getStructure()) { - if (uses.getMode() == StructureMapModelMode.TARGET) { - if (found) - throw new FHIRException("Multiple targets found in map "+map.getUrl()); - found = true; - res = worker.fetchResource(StructureDefinition.class, uses.getUrl()); - if (res == null) - throw new FHIRException("Unable to find "+uses.getUrl()+" referenced from map "+map.getUrl()); - } - } - if (res == null) - throw new FHIRException("No targets found in map "+map.getUrl()); - return res; - } - - public enum VariableMode { - INPUT, OUTPUT - } - - public class Variable { - private VariableMode mode; - private String name; - private Base object; - public Variable(VariableMode mode, String name, Base object) { - super(); - this.mode = mode; - this.name = name; - this.object = object; - } - public VariableMode getMode() { - return mode; - } - public String getName() { - return name; - } - public Base getObject() { - return object; - } - public String summary() { - return name+": "+object.fhirType(); - } - } - - public class Variables { - private List list = new ArrayList(); - - public void add(VariableMode mode, String name, Base object) { - Variable vv = null; - for (Variable v : list) - if ((v.mode == mode) && v.getName().equals(name)) - vv = v; - if (vv != null) - list.remove(vv); - list.add(new Variable(mode, name, object)); - } - - public Variables copy() { - Variables result = new Variables(); - result.list.addAll(list); - return result; - } - - public Base get(VariableMode mode, String name) { - for (Variable v : list) - if ((v.mode == mode) && v.getName().equals(name)) - return v.getObject(); - return null; - } - - public String summary() { - CommaSeparatedStringBuilder s = new CommaSeparatedStringBuilder(); - CommaSeparatedStringBuilder t = new CommaSeparatedStringBuilder(); - for (Variable v : list) - if (v.mode == VariableMode.INPUT) - s.append(v.summary()); - else - t.append(v.summary()); - return "source variables ["+s.toString()+"], target variables ["+t.toString()+"]"; - } - } - - public class TransformContext { - private Object appInfo; - - public TransformContext(Object appInfo) { - super(); - this.appInfo = appInfo; - } - - public Object getAppInfo() { - return appInfo; - } - - } - - private void log(String cnt) { - if (services != null) - services.log(cnt); - } - - /** - * 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 descendents - * - * @param item - * @param name - * @param result - * @throws FHIRException - */ - protected void getChildrenByName(Base item, String name, List result) throws FHIRException { - for (Base v : item.listChildrenByName(name, true)) - if (v != null) - result.add(v); - } - - public void transform(Object appInfo, Base source, StructureMap map, Base target) throws FHIRException { - TransformContext context = new TransformContext(appInfo); - log("Start Transform "+map.getUrl()); - StructureMapGroupComponent g = map.getGroup().get(0); - - Variables vars = new Variables(); - vars.add(VariableMode.INPUT, getInputName(g, StructureMapInputMode.SOURCE, "source"), source); - vars.add(VariableMode.OUTPUT, getInputName(g, StructureMapInputMode.TARGET, "target"), target); - - executeGroup("", context, map, vars, g); - if (target instanceof Element) - ((Element) target).sort(); - } - - private String getInputName(StructureMapGroupComponent g, StructureMapInputMode mode, String def) throws DefinitionException { - String name = null; - for (StructureMapGroupInputComponent inp : g.getInput()) { - if (inp.getMode() == mode) - if (name != null) - throw new DefinitionException("This engine does not support multiple source inputs"); - else - name = inp.getName(); - } - return name == null ? def : name; - } - - private void executeGroup(String indent, TransformContext context, StructureMap map, Variables vars, StructureMapGroupComponent group) throws FHIRException { - log(indent+"Group : "+group.getName()); - // todo: check inputs - if (group.hasExtends()) { - ResolvedGroup rg = resolveGroupReference(map, group, group.getExtends()); - executeGroup(indent+" ", context, rg.targetMap, vars, rg.target); - } - - for (StructureMapGroupRuleComponent r : group.getRule()) { - executeRule(indent+" ", context, map, vars, group, r); - } - } - - private void executeRule(String indent, TransformContext context, StructureMap map, Variables vars, StructureMapGroupComponent group, StructureMapGroupRuleComponent rule) throws FHIRException { - log(indent+"rule : "+rule.getName()); - if (rule.getName().contains("CarePlan.participant-unlink")) - System.out.println("debug"); - Variables srcVars = vars.copy(); - if (rule.getSource().size() != 1) - throw new FHIRException("Rule \""+rule.getName()+"\": not handled yet"); - List source = processSource(rule.getName(), context, srcVars, rule.getSource().get(0)); - if (source != null) { - for (Variables v : source) { - for (StructureMapGroupRuleTargetComponent t : rule.getTarget()) { - processTarget(rule.getName(), context, v, map, group, t, rule.getSource().size() == 1 ? rule.getSourceFirstRep().getVariable() : null); - } - if (rule.hasRule()) { - for (StructureMapGroupRuleComponent childrule : rule.getRule()) { - executeRule(indent +" ", context, map, v, group, childrule); - } - } else if (rule.hasDependent()) { - for (StructureMapGroupRuleDependentComponent dependent : rule.getDependent()) { - executeDependency(indent+" ", context, map, v, group, dependent); - } - } else if (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasVariable() && rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasVariable() && rule.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE && !rule.getTargetFirstRep().hasParameter()) { - // simple inferred, map by type - Base src = v.get(VariableMode.INPUT, rule.getSourceFirstRep().getVariable()); - Base tgt = v.get(VariableMode.OUTPUT, rule.getTargetFirstRep().getVariable()); - String srcType = src.fhirType(); - String tgtType = tgt.fhirType(); - ResolvedGroup defGroup = resolveGroupByTypes(map, rule.getName(), group, srcType, tgtType); - Variables vdef = new Variables(); - vdef.add(VariableMode.INPUT, defGroup.target.getInput().get(0).getName(), src); - vdef.add(VariableMode.OUTPUT, defGroup.target.getInput().get(1).getName(), tgt); - executeGroup(indent+" ", context, defGroup.targetMap, vdef, defGroup.target); - } - } - } - } - - private void executeDependency(String indent, TransformContext context, StructureMap map, Variables vin, StructureMapGroupComponent group, StructureMapGroupRuleDependentComponent dependent) throws FHIRException { - ResolvedGroup rg = resolveGroupReference(map, group, dependent.getName()); - - if (rg.target.getInput().size() != dependent.getVariable().size()) { - throw new FHIRException("Rule '"+dependent.getName()+"' has "+Integer.toString(rg.target.getInput().size())+" but the invocation has "+Integer.toString(dependent.getVariable().size())+" variables"); - } - Variables v = new Variables(); - for (int i = 0; i < rg.target.getInput().size(); i++) { - StructureMapGroupInputComponent input = rg.target.getInput().get(i); - StringType rdp = dependent.getVariable().get(i); - String var = rdp.asStringValue(); - VariableMode mode = input.getMode() == StructureMapInputMode.SOURCE ? VariableMode.INPUT : VariableMode.OUTPUT; - Base vv = vin.get(mode, var); - if (vv == null && mode == VariableMode.INPUT) //* once source, always source. but target can be treated as source at user convenient - vv = vin.get(VariableMode.OUTPUT, var); - if (vv == null) - throw new FHIRException("Rule '"+dependent.getName()+"' "+mode.toString()+" variable '"+input.getName()+"' named as '"+var+"' has no value"); - v.add(mode, input.getName(), vv); - } - executeGroup(indent+" ", context, rg.targetMap, v, rg.target); - } - - private String determineTypeFromSourceType(StructureMap map, StructureMapGroupComponent source, Base base, String[] types) throws FHIRException { - String type = base.fhirType(); - String kn = "type^"+type; - if (source.hasUserData(kn)) - return source.getUserString(kn); - - ResolvedGroup res = new ResolvedGroup(); - res.targetMap = null; - res.target = null; - for (StructureMapGroupComponent grp : map.getGroup()) { - if (matchesByType(map, grp, type)) { - if (res.targetMap == null) { - res.targetMap = map; - res.target = grp; - } else - throw new FHIRException("Multiple possible matches looking for default rule for '"+type+"'"); - } - } - if (res.targetMap != null) { - String result = getActualType(res.targetMap, res.target.getInput().get(1).getType()); - source.setUserData(kn, result); - return result; - } - - for (UriType imp : map.getImport()) { - List impMapList = findMatchingMaps(imp.getValue()); - if (impMapList.size() == 0) - throw new FHIRException("Unable to find map(s) for "+imp.getValue()); - for (StructureMap impMap : impMapList) { - if (!impMap.getUrl().equals(map.getUrl())) { - for (StructureMapGroupComponent grp : impMap.getGroup()) { - if (matchesByType(impMap, grp, type)) { - if (res.targetMap == null) { - res.targetMap = impMap; - res.target = grp; - } else - throw new FHIRException("Multiple possible matches for default rule for '"+type+"' in "+res.targetMap.getUrl()+" ("+res.target.getName()+") and "+impMap.getUrl()+" ("+grp.getName()+")"); - } - } - } - } - } - if (res.target == null) - throw new FHIRException("No matches found for default rule for '"+type+"' from "+map.getUrl()); - String result = getActualType(res.targetMap, res.target.getInput().get(1).getType()); // should be .getType, but R2... - source.setUserData(kn, result); - return result; - } - - private List findMatchingMaps(String value) { - List res = new ArrayList(); - if (value.contains("*")) { - for (StructureMap sm : library.values()) { - if (urlMatches(value, sm.getUrl())) { - res.add(sm); - } - } - } else { - StructureMap sm = library.get(value); - if (sm != null) - res.add(sm); - } - Set check = new HashSet(); - for (StructureMap sm : res) { - if (check.contains(sm.getUrl())) - throw new Error("duplicate"); - else - check.add(sm.getUrl()); - } - return res; - } - - private boolean urlMatches(String mask, String url) { - return url.length() > mask.length() && url.startsWith(mask.substring(0, mask.indexOf("*"))) && url.endsWith(mask.substring(mask.indexOf("*")+1)) ; - } - - private ResolvedGroup resolveGroupByTypes(StructureMap map, String ruleid, StructureMapGroupComponent source, String srcType, String tgtType) throws FHIRException { - String kn = "types^"+srcType+":"+tgtType; - if (source.hasUserData(kn)) - return (ResolvedGroup) source.getUserData(kn); - - ResolvedGroup res = new ResolvedGroup(); - res.targetMap = null; - res.target = null; - for (StructureMapGroupComponent grp : map.getGroup()) { - if (matchesByType(map, grp, srcType, tgtType)) { - if (res.targetMap == null) { - res.targetMap = map; - res.target = grp; - } else - throw new FHIRException("Multiple possible matches looking for rule for '"+srcType+"/"+tgtType+"', from rule '"+ruleid+"'"); - } - } - if (res.targetMap != null) { - source.setUserData(kn, res); - return res; - } - - for (UriType imp : map.getImport()) { - List impMapList = findMatchingMaps(imp.getValue()); - if (impMapList.size() == 0) - throw new FHIRException("Unable to find map(s) for "+imp.getValue()); - for (StructureMap impMap : impMapList) { - if (!impMap.getUrl().equals(map.getUrl())) { - for (StructureMapGroupComponent grp : impMap.getGroup()) { - if (matchesByType(impMap, grp, srcType, tgtType)) { - if (res.targetMap == null) { - res.targetMap = impMap; - res.target = grp; - } else - throw new FHIRException("Multiple possible matches for rule for '"+srcType+"/"+tgtType+"' in "+res.targetMap.getUrl()+" and "+impMap.getUrl()+", from rule '"+ruleid+"'"); - } - } - } - } - } - if (res.target == null) - throw new FHIRException("No matches found for rule for '"+srcType+"/"+tgtType+"' from "+map.getUrl()+", from rule '"+ruleid+"'"); - source.setUserData(kn, res); - return res; - } - - - private boolean matchesByType(StructureMap map, StructureMapGroupComponent grp, String type) throws FHIRException { - if (grp.getTypeMode() != StructureMapGroupTypeMode.TYPEANDTYPES) - return false; - if (grp.getInput().size() != 2 || grp.getInput().get(0).getMode() != StructureMapInputMode.SOURCE || grp.getInput().get(1).getMode() != StructureMapInputMode.TARGET) - return false; - return matchesType(map, type, grp.getInput().get(0).getType()); - } - - private boolean matchesByType(StructureMap map, StructureMapGroupComponent grp, String srcType, String tgtType) throws FHIRException { - if (grp.getTypeMode() == StructureMapGroupTypeMode.NONE) - return false; - if (grp.getInput().size() != 2 || grp.getInput().get(0).getMode() != StructureMapInputMode.SOURCE || grp.getInput().get(1).getMode() != StructureMapInputMode.TARGET) - return false; - if (!grp.getInput().get(0).hasType() || !grp.getInput().get(1).hasType()) - return false; - return matchesType(map, srcType, grp.getInput().get(0).getType()) && matchesType(map, tgtType, grp.getInput().get(1).getType()); - } - - private boolean matchesType(StructureMap map, String actualType, String statedType) throws FHIRException { - // check the aliases - for (StructureMapStructureComponent imp : map.getStructure()) { - if (imp.hasAlias() && statedType.equals(imp.getAlias())) { - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, imp.getUrl()); - if (sd != null) - statedType = sd.getType(); - break; - } - } - - return actualType.equals(statedType); - } - - private String getActualType(StructureMap map, String statedType) throws FHIRException { - // check the aliases - for (StructureMapStructureComponent imp : map.getStructure()) { - if (imp.hasAlias() && statedType.equals(imp.getAlias())) { - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, imp.getUrl()); - if (sd == null) - throw new FHIRException("Unable to resolve structure "+imp.getUrl()); - return sd.getId(); // should be sd.getType(), but R2... - } - } - return statedType; - } - - - private ResolvedGroup resolveGroupReference(StructureMap map, StructureMapGroupComponent source, String name) throws FHIRException { - String kn = "ref^"+name; - if (source.hasUserData(kn)) - return (ResolvedGroup) source.getUserData(kn); - - ResolvedGroup res = new ResolvedGroup(); - res.targetMap = null; - res.target = null; - for (StructureMapGroupComponent grp : map.getGroup()) { - if (grp.getName().equals(name)) { - if (res.targetMap == null) { - res.targetMap = map; - res.target = grp; - } else - throw new FHIRException("Multiple possible matches for rule '"+name+"'"); - } - } - if (res.targetMap != null) { - source.setUserData(kn, res); - return res; - } - - for (UriType imp : map.getImport()) { - List impMapList = findMatchingMaps(imp.getValue()); - if (impMapList.size() == 0) - throw new FHIRException("Unable to find map(s) for "+imp.getValue()); - for (StructureMap impMap : impMapList) { - if (!impMap.getUrl().equals(map.getUrl())) { - for (StructureMapGroupComponent grp : impMap.getGroup()) { - if (grp.getName().equals(name)) { - if (res.targetMap == null) { - res.targetMap = impMap; - res.target = grp; - } else - throw new FHIRException("Multiple possible matches for rule '"+name+"' in "+res.targetMap.getUrl()+" and "+impMap.getUrl()); - } - } - } - } - } - if (res.target == null) - throw new FHIRException("No matches found for rule '"+name+"'. Reference found in "+map.getUrl()); - source.setUserData(kn, res); - return res; - } - - private List processSource(String ruleId, TransformContext context, Variables vars, StructureMapGroupRuleSourceComponent src) throws FHIRException { - List items; - if (src.getContext().equals("@search")) { - ExpressionNode expr = (ExpressionNode) src.getUserData(MAP_SEARCH_EXPRESSION); - if (expr == null) { - expr = fpe.parse(src.getElement()); - src.setUserData(MAP_SEARCH_EXPRESSION, expr); - } - String search = fpe.evaluateToString(vars, null, new StringType(), expr); // string is a holder of nothing to ensure that variables are processed correctly - items = services.performSearch(context.appInfo, search); - } else { - items = new ArrayList(); - Base b = vars.get(VariableMode.INPUT, src.getContext()); - if (b == null) - throw new FHIRException("Unknown input variable "+src.getContext()); - - if (!src.hasElement()) - items.add(b); - else { - getChildrenByName(b, src.getElement(), items); - if (items.size() == 0 && src.hasDefaultValue()) - items.add(src.getDefaultValue()); - } - } - - if (src.hasType()) { - List remove = new ArrayList(); - for (Base item : items) { - if (item != null && !isType(item, src.getType())) { - remove.add(item); - } - } - items.removeAll(remove); - } - - if (src.hasCondition()) { - ExpressionNode expr = (ExpressionNode) src.getUserData(MAP_WHERE_EXPRESSION); - if (expr == null) { - expr = fpe.parse(src.getCondition()); - // fpe.check(context.appInfo, ??, ??, expr) - src.setUserData(MAP_WHERE_EXPRESSION, expr); - } - List remove = new ArrayList(); - for (Base item : items) { - if (!fpe.evaluateToBoolean(vars, null, item, expr)) - remove.add(item); - } - items.removeAll(remove); - } - - if (src.hasCheck()) { - ExpressionNode expr = (ExpressionNode) src.getUserData(MAP_WHERE_CHECK); - if (expr == null) { - expr = fpe.parse(src.getCheck()); - // fpe.check(context.appInfo, ??, ??, expr) - src.setUserData(MAP_WHERE_CHECK, expr); - } - List remove = new ArrayList(); - for (Base item : items) { - if (!fpe.evaluateToBoolean(vars, null, item, expr)) - throw new FHIRException("Rule \""+ruleId+"\": Check condition failed"); - } - } - - - if (src.hasListMode() && !items.isEmpty()) { - switch (src.getListMode()) { - case FIRST: - Base bt = items.get(0); - items.clear(); - items.add(bt); - break; - case NOTFIRST: - if (items.size() > 0) - items.remove(0); - break; - case LAST: - bt = items.get(items.size()-1); - items.clear(); - items.add(bt); - break; - case NOTLAST: - if (items.size() > 0) - items.remove(items.size()-1); - break; - case ONLYONE: - if (items.size() > 1) - throw new FHIRException("Rule \""+ruleId+"\": Check condition failed: the collection has more than one item"); - break; - case NULL: - } - } - List result = new ArrayList(); - for (Base r : items) { - Variables v = vars.copy(); - if (src.hasVariable()) - v.add(VariableMode.INPUT, src.getVariable(), r); - result.add(v); - } - return result; - } - - - private boolean isType(Base item, String type) { - if (type.equals(item.fhirType())) - return true; - return false; - } - - private void processTarget(String ruleId, TransformContext context, Variables vars, StructureMap map, StructureMapGroupComponent group, StructureMapGroupRuleTargetComponent tgt, String srcVar) throws FHIRException { - Base dest = null; - if (tgt.hasContext()) { - dest = vars.get(VariableMode.OUTPUT, tgt.getContext()); - if (dest == null) - throw new FHIRException("Rule \""+ruleId+"\": target context not known: "+tgt.getContext()); - if (!tgt.hasElement()) - throw new FHIRException("Rule \""+ruleId+"\": Not supported yet"); - } - Base v = null; - if (tgt.hasTransform()) { - v = runTransform(ruleId, context, map, group, tgt, vars, dest, tgt.getElement(), srcVar); - if (v != null && dest != null) - v = dest.setProperty(tgt.getElement().hashCode(), tgt.getElement(), v); // reset v because some implementations may have to rewrite v when setting the value - } else if (dest != null) - v = dest.makeProperty(tgt.getElement().hashCode(), tgt.getElement()); - if (tgt.hasVariable() && v != null) - vars.add(VariableMode.OUTPUT, tgt.getVariable(), v); - } - - private Base runTransform(String ruleId, TransformContext context, StructureMap map, StructureMapGroupComponent group, StructureMapGroupRuleTargetComponent tgt, Variables vars, Base dest, String element, String srcVar) throws FHIRException { - try { - switch (tgt.getTransform()) { - case CREATE : - String tn; - if (tgt.getParameter().isEmpty()) { - // we have to work out the type. First, we see if there is a single type for the target. If there is, we use that - String[] types = dest.getTypesForProperty(element.hashCode(), element); - if (types.length == 1 && !"*".equals(types[0]) && !types[0].equals("Resource")) - tn = types[0]; - else if (srcVar != null) { - tn = determineTypeFromSourceType(map, group, vars.get(VariableMode.INPUT, srcVar), types); - } else - throw new Error("Cannot determine type implicitly because there is no single input variable"); - } else - tn = getParamStringNoNull(vars, tgt.getParameter().get(0), tgt.toString()); - Base res = services != null ? services.createType(context.getAppInfo(), tn) : ResourceFactory.createResourceOrType(tn); - if (res.isResource() && !res.fhirType().equals("Parameters")) { -// res.setIdBase(tgt.getParameter().size() > 1 ? getParamString(vars, tgt.getParameter().get(0)) : UUID.randomUUID().toString().toLowerCase()); - if (services != null) - res = services.createResource(context.getAppInfo(), res); - } - if (tgt.hasUserData("profile")) - res.setUserData("profile", tgt.getUserData("profile")); - return res; - case COPY : - return getParam(vars, tgt.getParameter().get(0)); - case EVALUATE : - ExpressionNode expr = (ExpressionNode) tgt.getUserData(MAP_EXPRESSION); - if (expr == null) { - expr = fpe.parse(getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString())); - tgt.setUserData(MAP_WHERE_EXPRESSION, expr); - } - List v = fpe.evaluate(vars, null, tgt.getParameter().size() == 2 ? getParam(vars, tgt.getParameter().get(0)) : new BooleanType(false), expr); - if (v.size() == 0) - return null; - else if (v.size() != 1) - throw new FHIRException("Rule \""+ruleId+"\": Evaluation of "+expr.toString()+" returned "+Integer.toString(v.size())+" objects"); - else - return v.get(0); - - case TRUNCATE : - String src = getParamString(vars, tgt.getParameter().get(0)); - String len = getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString()); - if (Utilities.isInteger(len)) { - int l = Integer.parseInt(len); - if (src.length() > l) - src = src.substring(0, l); - } - return new StringType(src); - case ESCAPE : - throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); - case CAST : - throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); - case APPEND : - throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); - case TRANSLATE : - return translate(context, map, vars, tgt.getParameter()); - case REFERENCE : - Base b = getParam(vars, tgt.getParameter().get(0)); - if (b == null) - throw new FHIRException("Rule \""+ruleId+"\": Unable to find parameter "+((IdType) tgt.getParameter().get(0).getValue()).asStringValue()); - if (!b.isResource()) - throw new FHIRException("Rule \""+ruleId+"\": Transform engine cannot point at an element of type "+b.fhirType()); - else { - String id = b.getIdBase(); - if (id == null) { - id = UUID.randomUUID().toString().toLowerCase(); - b.setIdBase(id); - } - return new Reference().setReference(b.fhirType()+"/"+id); - } - case DATEOP : - throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); - case UUID : - return new IdType(UUID.randomUUID().toString()); - case POINTER : - b = getParam(vars, tgt.getParameter().get(0)); - if (b instanceof Resource) - return new UriType("urn:uuid:"+((Resource) b).getId()); - else - throw new FHIRException("Rule \""+ruleId+"\": Transform engine cannot point at an element of type "+b.fhirType()); - case CC: - CodeableConcept cc = new CodeableConcept(); - cc.addCoding(buildCoding(getParamStringNoNull(vars, tgt.getParameter().get(0), tgt.toString()), getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString()))); - return cc; - case C: - Coding c = buildCoding(getParamStringNoNull(vars, tgt.getParameter().get(0), tgt.toString()), getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString())); - return c; - default: - throw new Error("Rule \""+ruleId+"\": Transform Unknown: "+tgt.getTransform().toCode()); - } - } catch (Exception e) { - throw new FHIRException("Exception executing transform "+tgt.toString()+" on Rule \""+ruleId+"\": "+e.getMessage(), e); - } - } - - - private Coding buildCoding(String uri, String code) throws FHIRException { - // if we can get this as a valueSet, we will - String system = null; - String display = null; - ValueSet vs = Utilities.noString(uri) ? null : worker.fetchResourceWithException(ValueSet.class, uri); - if (vs != null) { - ValueSetExpansionOutcome vse = worker.expandVS(vs, true, false); - if (vse.getError() != null) - throw new FHIRException(vse.getError()); - CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); - for (ValueSetExpansionContainsComponent t : vse.getValueset().getExpansion().getContains()) { - if (t.hasCode()) - b.append(t.getCode()); - if (code.equals(t.getCode()) && t.hasSystem()) { - system = t.getSystem(); - display = t.getDisplay(); - break; - } - if (code.equalsIgnoreCase(t.getDisplay()) && t.hasSystem()) { - system = t.getSystem(); - display = t.getDisplay(); - break; - } - } - if (system == null) - throw new FHIRException("The code '"+code+"' is not in the value set '"+uri+"' (valid codes: "+b.toString()+"; also checked displays)"); - } else - system = uri; - ValidationResult vr = worker.validateCode(system, code, null); - if (vr != null && vr.getDisplay() != null) - display = vr.getDisplay(); - return new Coding().setSystem(system).setCode(code).setDisplay(display); - } - - - private String getParamStringNoNull(Variables vars, StructureMapGroupRuleTargetParameterComponent parameter, String message) throws FHIRException { - Base b = getParam(vars, parameter); - if (b == null) - throw new FHIRException("Unable to find a value for "+parameter.toString()+". Context: "+message); - if (!b.hasPrimitiveValue()) - throw new FHIRException("Found a value for "+parameter.toString()+", but it has a type of "+b.fhirType()+" and cannot be treated as a string. Context: "+message); - return b.primitiveValue(); - } - - private String getParamString(Variables vars, StructureMapGroupRuleTargetParameterComponent parameter) throws DefinitionException { - Base b = getParam(vars, parameter); - if (b == null || !b.hasPrimitiveValue()) - return null; - return b.primitiveValue(); - } - - - private Base getParam(Variables vars, StructureMapGroupRuleTargetParameterComponent parameter) throws DefinitionException { - Type p = parameter.getValue(); - if (!(p instanceof IdType)) - return p; - else { - String n = ((IdType) p).asStringValue(); - Base b = vars.get(VariableMode.INPUT, n); - if (b == null) - b = vars.get(VariableMode.OUTPUT, n); - if (b == null) - throw new DefinitionException("Variable "+n+" not found ("+vars.summary()+")"); - return b; - } - } - - - private Base translate(TransformContext context, StructureMap map, Variables vars, List parameter) throws FHIRException { - Base src = getParam(vars, parameter.get(0)); - String id = getParamString(vars, parameter.get(1)); - String fld = parameter.size() > 2 ? getParamString(vars, parameter.get(2)) : null; - return translate(context, map, src, id, fld); - } - - private class SourceElementComponentWrapper { - private ConceptMapGroupComponent group; - private SourceElementComponent comp; - public SourceElementComponentWrapper(ConceptMapGroupComponent group, SourceElementComponent comp) { - super(); - this.group = group; - this.comp = comp; - } - } - public Base translate(TransformContext context, StructureMap map, Base source, String conceptMapUrl, String fieldToReturn) throws FHIRException { - Coding src = new Coding(); - if (source.isPrimitive()) { - src.setCode(source.primitiveValue()); - } else if ("Coding".equals(source.fhirType())) { - Base[] b = source.getProperty("system".hashCode(), "system", true); - if (b.length == 1) - src.setSystem(b[0].primitiveValue()); - b = source.getProperty("code".hashCode(), "code", true); - if (b.length == 1) - src.setCode(b[0].primitiveValue()); - } else if ("CE".equals(source.fhirType())) { - Base[] b = source.getProperty("codeSystem".hashCode(), "codeSystem", true); - if (b.length == 1) - src.setSystem(b[0].primitiveValue()); - b = source.getProperty("code".hashCode(), "code", true); - if (b.length == 1) - src.setCode(b[0].primitiveValue()); - } else - throw new FHIRException("Unable to translate source "+source.fhirType()); - - String su = conceptMapUrl; - if (conceptMapUrl.equals("http://hl7.org/fhir/ConceptMap/special-oid2uri")) { - String uri = worker.oid2Uri(src.getCode()); - if (uri == null) - uri = "urn:oid:"+src.getCode(); - if ("uri".equals(fieldToReturn)) - return new UriType(uri); - else - throw new FHIRException("Error in return code"); - } else { - ConceptMap cmap = null; - if (conceptMapUrl.startsWith("#")) { - for (Resource r : map.getContained()) { - if (r instanceof ConceptMap && ((ConceptMap) r).getId().equals(conceptMapUrl.substring(1))) { - cmap = (ConceptMap) r; - su = map.getUrl()+conceptMapUrl; - } - } - if (cmap == null) - throw new FHIRException("Unable to translate - cannot find map "+conceptMapUrl); - } else - cmap = worker.fetchResource(ConceptMap.class, conceptMapUrl); - Coding outcome = null; - boolean done = false; - String message = null; - if (cmap == null) { - if (services == null) - message = "No map found for "+conceptMapUrl; - else { - outcome = services.translate(context.appInfo, src, conceptMapUrl); - done = true; - } - } else { - List list = new ArrayList(); - for (ConceptMapGroupComponent g : cmap.getGroup()) { - for (SourceElementComponent e : g.getElement()) { - if (!src.hasSystem() && src.getCode().equals(e.getCode())) - list.add(new SourceElementComponentWrapper(g, e)); - else if (src.hasSystem() && src.getSystem().equals(g.getSource()) && src.getCode().equals(e.getCode())) - list.add(new SourceElementComponentWrapper(g, e)); - } - } - if (list.size() == 0) - done = true; - else if (list.get(0).comp.getTarget().size() == 0) - message = "Concept map "+su+" found no translation for "+src.getCode(); - else { - for (TargetElementComponent tgt : list.get(0).comp.getTarget()) { - if (tgt.getEquivalence() == null || EnumSet.of( ConceptMapEquivalence.EQUAL , ConceptMapEquivalence.RELATEDTO , ConceptMapEquivalence.EQUIVALENT, ConceptMapEquivalence.WIDER).contains(tgt.getEquivalence())) { - if (done) { - message = "Concept map "+su+" found multiple matches for "+src.getCode(); - done = false; - } else { - done = true; - outcome = new Coding().setCode(tgt.getCode()).setSystem(list.get(0).group.getTarget()); - } - } else if (tgt.getEquivalence() == ConceptMapEquivalence.UNMATCHED) { - done = true; - } - } - if (!done) - message = "Concept map "+su+" found no usable translation for "+src.getCode(); - } - } - if (!done) - throw new FHIRException(message); - if (outcome == null) - return null; - if ("code".equals(fieldToReturn)) - return new CodeType(outcome.getCode()); - else - return outcome; - } - } - - - public Map getLibrary() { - return library; - } - - public class PropertyWithType { - private String path; - private Property baseProperty; - private Property profileProperty; - private TypeDetails types; - public PropertyWithType(String path, Property baseProperty, Property profileProperty, TypeDetails types) { - super(); - this.baseProperty = baseProperty; - this.profileProperty = profileProperty; - this.path = path; - this.types = types; - } - - public TypeDetails getTypes() { - return types; - } - public String getPath() { - return path; - } - - public Property getBaseProperty() { - return baseProperty; - } - - public void setBaseProperty(Property baseProperty) { - this.baseProperty = baseProperty; - } - - public Property getProfileProperty() { - return profileProperty; - } - - public void setProfileProperty(Property profileProperty) { - this.profileProperty = profileProperty; - } - - public String summary() { - return path; - } - - } - - public class VariableForProfiling { - private VariableMode mode; - private String name; - private PropertyWithType property; - - public VariableForProfiling(VariableMode mode, String name, PropertyWithType property) { - super(); - this.mode = mode; - this.name = name; - this.property = property; - } - public VariableMode getMode() { - return mode; - } - public String getName() { - return name; - } - public PropertyWithType getProperty() { - return property; - } - public String summary() { - return name+": "+property.summary(); - } - } - - public class VariablesForProfiling { - private List list = new ArrayList(); - private boolean optional; - private boolean repeating; - - public VariablesForProfiling(boolean optional, boolean repeating) { - this.optional = optional; - this.repeating = repeating; - } - - public void add(VariableMode mode, String name, String path, Property property, TypeDetails types) { - add(mode, name, new PropertyWithType(path, property, null, types)); - } - - public void add(VariableMode mode, String name, String path, Property baseProperty, Property profileProperty, TypeDetails types) { - add(mode, name, new PropertyWithType(path, baseProperty, profileProperty, types)); - } - - public void add(VariableMode mode, String name, PropertyWithType property) { - VariableForProfiling vv = null; - for (VariableForProfiling v : list) - if ((v.mode == mode) && v.getName().equals(name)) - vv = v; - if (vv != null) - list.remove(vv); - list.add(new VariableForProfiling(mode, name, property)); - } - - public VariablesForProfiling copy(boolean optional, boolean repeating) { - VariablesForProfiling result = new VariablesForProfiling(optional, repeating); - result.list.addAll(list); - return result; - } - - public VariablesForProfiling copy() { - VariablesForProfiling result = new VariablesForProfiling(optional, repeating); - result.list.addAll(list); - return result; - } - - public VariableForProfiling get(VariableMode mode, String name) { - if (mode == null) { - for (VariableForProfiling v : list) - if ((v.mode == VariableMode.OUTPUT) && v.getName().equals(name)) - return v; - for (VariableForProfiling v : list) - if ((v.mode == VariableMode.INPUT) && v.getName().equals(name)) - return v; - } - for (VariableForProfiling v : list) - if ((v.mode == mode) && v.getName().equals(name)) - return v; - return null; - } - - public String summary() { - CommaSeparatedStringBuilder s = new CommaSeparatedStringBuilder(); - CommaSeparatedStringBuilder t = new CommaSeparatedStringBuilder(); - for (VariableForProfiling v : list) - if (v.mode == VariableMode.INPUT) - s.append(v.summary()); - else - t.append(v.summary()); - return "source variables ["+s.toString()+"], target variables ["+t.toString()+"]"; - } - } - - public class StructureMapAnalysis { - private List profiles = new ArrayList(); - private XhtmlNode summary; - public List getProfiles() { - return profiles; - } - public XhtmlNode getSummary() { - return summary; - } - - } - - /** - * Given a structure map, return a set of analyses on it. - * - * Returned: - * - a list or profiles for what it will create. First profile is the target - * - a table with a summary (in xhtml) for easy human undertanding of the mapping - * - * - * @param appInfo - * @param map - * @return - * @throws Exception - */ - public StructureMapAnalysis analyse(Object appInfo, StructureMap map) throws Exception { - ids.clear(); - StructureMapAnalysis result = new StructureMapAnalysis(); - TransformContext context = new TransformContext(appInfo); - VariablesForProfiling vars = new VariablesForProfiling(false, false); - StructureMapGroupComponent start = map.getGroup().get(0); - for (StructureMapGroupInputComponent t : start.getInput()) { - PropertyWithType ti = resolveType(map, t.getType(), t.getMode()); - if (t.getMode() == StructureMapInputMode.SOURCE) - vars.add(VariableMode.INPUT, t.getName(), ti); - else - vars.add(VariableMode.OUTPUT, t.getName(), createProfile(map, result.profiles, ti, start.getName(), start)); - } - - result.summary = new XhtmlNode(NodeType.Element, "table").setAttribute("class", "grid"); - XhtmlNode tr = result.summary.addTag("tr"); - tr.addTag("td").addTag("b").addText("Source"); - tr.addTag("td").addTag("b").addText("Target"); - - log("Start Profiling Transform "+map.getUrl()); - analyseGroup("", context, map, vars, start, result); - ProfileUtilities pu = new ProfileUtilities(worker, null, pkp); - for (StructureDefinition sd : result.getProfiles()) - pu.cleanUpDifferential(sd); - return result; - } - - - private void analyseGroup(String indent, TransformContext context, StructureMap map, VariablesForProfiling vars, StructureMapGroupComponent group, StructureMapAnalysis result) throws Exception { - log(indent+"Analyse Group : "+group.getName()); - // todo: extends - // todo: check inputs - XhtmlNode tr = result.summary.addTag("tr").setAttribute("class", "diff-title"); - XhtmlNode xs = tr.addTag("td"); - XhtmlNode xt = tr.addTag("td"); - for (StructureMapGroupInputComponent inp : group.getInput()) { - if (inp.getMode() == StructureMapInputMode.SOURCE) - noteInput(vars, inp, VariableMode.INPUT, xs); - if (inp.getMode() == StructureMapInputMode.TARGET) - noteInput(vars, inp, VariableMode.OUTPUT, xt); - } - for (StructureMapGroupRuleComponent r : group.getRule()) { - analyseRule(indent+" ", context, map, vars, group, r, result); - } - } - - - private void noteInput(VariablesForProfiling vars, StructureMapGroupInputComponent inp, VariableMode mode, XhtmlNode xs) { - VariableForProfiling v = vars.get(mode, inp.getName()); - if (v != null) - xs.addText("Input: "+v.property.getPath()); - } - - private void analyseRule(String indent, TransformContext context, StructureMap map, VariablesForProfiling vars, StructureMapGroupComponent group, StructureMapGroupRuleComponent rule, StructureMapAnalysis result) throws Exception { - log(indent+"Analyse rule : "+rule.getName()); - XhtmlNode tr = result.summary.addTag("tr"); - XhtmlNode xs = tr.addTag("td"); - XhtmlNode xt = tr.addTag("td"); - - VariablesForProfiling srcVars = vars.copy(); - if (rule.getSource().size() != 1) - throw new Exception("Rule \""+rule.getName()+"\": not handled yet"); - VariablesForProfiling source = analyseSource(rule.getName(), context, srcVars, rule.getSourceFirstRep(), xs); - - TargetWriter tw = new TargetWriter(); - for (StructureMapGroupRuleTargetComponent t : rule.getTarget()) { - analyseTarget(rule.getName(), context, source, map, t, rule.getSourceFirstRep().getVariable(), tw, result.profiles, rule.getName()); - } - tw.commit(xt); - - for (StructureMapGroupRuleComponent childrule : rule.getRule()) { - analyseRule(indent+" ", context, map, source, group, childrule, result); - } -// for (StructureMapGroupRuleDependentComponent dependent : rule.getDependent()) { -// executeDependency(indent+" ", context, map, v, group, dependent); // do we need group here? -// } - } - - public class StringPair { - private String var; - private String desc; - public StringPair(String var, String desc) { - super(); - this.var = var; - this.desc = desc; - } - public String getVar() { - return var; - } - public String getDesc() { - return desc; - } - } - public class TargetWriter { - private Map newResources = new HashMap(); - private List assignments = new ArrayList(); - private List keyProps = new ArrayList(); - private CommaSeparatedStringBuilder txt = new CommaSeparatedStringBuilder(); - - public void newResource(String var, String name) { - newResources.put(var, name); - txt.append("new "+name); - } - - public void valueAssignment(String context, String desc) { - assignments.add(new StringPair(context, desc)); - txt.append(desc); - } - - public void keyAssignment(String context, String desc) { - keyProps.add(new StringPair(context, desc)); - txt.append(desc); - } - public void commit(XhtmlNode xt) { - if (newResources.size() == 1 && assignments.size() == 1 && newResources.containsKey(assignments.get(0).getVar()) && keyProps.size() == 1 && newResources.containsKey(keyProps.get(0).getVar()) ) { - xt.addText("new "+assignments.get(0).desc+" ("+keyProps.get(0).desc.substring(keyProps.get(0).desc.indexOf(".")+1)+")"); - } else if (newResources.size() == 1 && assignments.size() == 1 && newResources.containsKey(assignments.get(0).getVar()) && keyProps.size() == 0) { - xt.addText("new "+assignments.get(0).desc); - } else { - xt.addText(txt.toString()); - } - } - } - - private VariablesForProfiling analyseSource(String ruleId, TransformContext context, VariablesForProfiling vars, StructureMapGroupRuleSourceComponent src, XhtmlNode td) throws Exception { - VariableForProfiling var = vars.get(VariableMode.INPUT, src.getContext()); - if (var == null) - throw new FHIRException("Rule \""+ruleId+"\": Unknown input variable "+src.getContext()); - PropertyWithType prop = var.getProperty(); - - boolean optional = false; - boolean repeating = false; - - if (src.hasCondition()) { - optional = true; - } - - if (src.hasElement()) { - Property element = prop.getBaseProperty().getChild(prop.types.getType(), src.getElement()); - if (element == null) - throw new Exception("Rule \""+ruleId+"\": Unknown element name "+src.getElement()); - if (element.getDefinition().getMin() == 0) - optional = true; - if (element.getDefinition().getMax().equals("*")) - repeating = true; - VariablesForProfiling result = vars.copy(optional, repeating); - TypeDetails type = new TypeDetails(CollectionStatus.SINGLETON); - for (TypeRefComponent tr : element.getDefinition().getType()) { - if (!tr.hasCode()) - throw new Error("Rule \""+ruleId+"\": Element has no type"); - ProfiledType pt = new ProfiledType(tr.getCode()); - if (tr.hasProfile()) - pt.addProfile(tr.getProfile()); - if (element.getDefinition().hasBinding()) - pt.addBinding(element.getDefinition().getBinding()); - type.addType(pt); - } - td.addText(prop.getPath()+"."+src.getElement()); - if (src.hasVariable()) - result.add(VariableMode.INPUT, src.getVariable(), new PropertyWithType(prop.getPath()+"."+src.getElement(), element, null, type)); - return result; - } else { - td.addText(prop.getPath()); // ditto! - return vars.copy(optional, repeating); - } - } - - - private void analyseTarget(String ruleId, TransformContext context, VariablesForProfiling vars, StructureMap map, StructureMapGroupRuleTargetComponent tgt, String tv, TargetWriter tw, List profiles, String sliceName) throws Exception { - VariableForProfiling var = null; - if (tgt.hasContext()) { - var = vars.get(VariableMode.OUTPUT, tgt.getContext()); - if (var == null) - throw new Exception("Rule \""+ruleId+"\": target context not known: "+tgt.getContext()); - if (!tgt.hasElement()) - throw new Exception("Rule \""+ruleId+"\": Not supported yet"); - } - - - TypeDetails type = null; - if (tgt.hasTransform()) { - type = analyseTransform(context, map, tgt, var, vars); - // profiling: dest.setProperty(tgt.getElement().hashCode(), tgt.getElement(), v); - } else { - Property vp = var.property.baseProperty.getChild(tgt.getElement(), tgt.getElement()); - if (vp == null) - throw new Exception("Unknown Property "+tgt.getElement()+" on "+var.property.path); - - type = new TypeDetails(CollectionStatus.SINGLETON, vp.getType(tgt.getElement())); - } - - if (tgt.getTransform() == StructureMapTransform.CREATE) { - String s = getParamString(vars, tgt.getParameter().get(0)); - if (worker.getResourceNames().contains(s)) - tw.newResource(tgt.getVariable(), s); - } else { - boolean mapsSrc = false; - for (StructureMapGroupRuleTargetParameterComponent p : tgt.getParameter()) { - Type pr = p.getValue(); - if (pr instanceof IdType && ((IdType) pr).asStringValue().equals(tv)) - mapsSrc = true; - } - if (mapsSrc) { - if (var == null) - throw new Error("Rule \""+ruleId+"\": Attempt to assign with no context"); - tw.valueAssignment(tgt.getContext(), var.property.getPath()+"."+tgt.getElement()+getTransformSuffix(tgt.getTransform())); - } else if (tgt.hasContext()) { - if (isSignificantElement(var.property, tgt.getElement())) { - String td = describeTransform(tgt); - if (td != null) - tw.keyAssignment(tgt.getContext(), var.property.getPath()+"."+tgt.getElement()+" = "+td); - } - } - } - Type fixed = generateFixedValue(tgt); - - PropertyWithType prop = updateProfile(var, tgt.getElement(), type, map, profiles, sliceName, fixed, tgt); - if (tgt.hasVariable()) - if (tgt.hasElement()) - vars.add(VariableMode.OUTPUT, tgt.getVariable(), prop); - else - vars.add(VariableMode.OUTPUT, tgt.getVariable(), prop); - } - - private Type generateFixedValue(StructureMapGroupRuleTargetComponent tgt) { - if (!allParametersFixed(tgt)) - return null; - if (!tgt.hasTransform()) - return null; - switch (tgt.getTransform()) { - case COPY: return tgt.getParameter().get(0).getValue(); - case TRUNCATE: return null; - //case ESCAPE: - //case CAST: - //case APPEND: - case TRANSLATE: return null; - //case DATEOP, - //case UUID, - //case POINTER, - //case EVALUATE, - case CC: - CodeableConcept cc = new CodeableConcept(); - cc.addCoding(buildCoding(tgt.getParameter().get(0).getValue(), tgt.getParameter().get(1).getValue())); - return cc; - case C: - return buildCoding(tgt.getParameter().get(0).getValue(), tgt.getParameter().get(1).getValue()); - case QTY: return null; - //case ID, - //case CP, - default: - return null; - } - } - - @SuppressWarnings("rawtypes") - private Coding buildCoding(Type value1, Type value2) { - return new Coding().setSystem(((PrimitiveType) value1).asStringValue()).setCode(((PrimitiveType) value2).asStringValue()) ; - } - - private boolean allParametersFixed(StructureMapGroupRuleTargetComponent tgt) { - for (StructureMapGroupRuleTargetParameterComponent p : tgt.getParameter()) { - Type pr = p.getValue(); - if (pr instanceof IdType) - return false; - } - return true; - } - - private String describeTransform(StructureMapGroupRuleTargetComponent tgt) throws FHIRException { - switch (tgt.getTransform()) { - case COPY: return null; - case TRUNCATE: return null; - //case ESCAPE: - //case CAST: - //case APPEND: - case TRANSLATE: return null; - //case DATEOP, - //case UUID, - //case POINTER, - //case EVALUATE, - case CC: return describeTransformCCorC(tgt); - case C: return describeTransformCCorC(tgt); - case QTY: return null; - //case ID, - //case CP, - default: - return null; - } - } - - @SuppressWarnings("rawtypes") - private String describeTransformCCorC(StructureMapGroupRuleTargetComponent tgt) throws FHIRException { - if (tgt.getParameter().size() < 2) - return null; - Type p1 = tgt.getParameter().get(0).getValue(); - Type p2 = tgt.getParameter().get(1).getValue(); - if (p1 instanceof IdType || p2 instanceof IdType) - return null; - if (!(p1 instanceof PrimitiveType) || !(p2 instanceof PrimitiveType)) - return null; - String uri = ((PrimitiveType) p1).asStringValue(); - String code = ((PrimitiveType) p2).asStringValue(); - if (Utilities.noString(uri)) - throw new FHIRException("Describe Transform, but the uri is blank"); - if (Utilities.noString(code)) - throw new FHIRException("Describe Transform, but the code is blank"); - Coding c = buildCoding(uri, code); - return NarrativeGenerator.describeSystem(c.getSystem())+"#"+c.getCode()+(c.hasDisplay() ? "("+c.getDisplay()+")" : ""); - } - - - private boolean isSignificantElement(PropertyWithType property, String element) { - if ("Observation".equals(property.getPath())) - return "code".equals(element); - else if ("Bundle".equals(property.getPath())) - return "type".equals(element); - else - return false; - } - - private String getTransformSuffix(StructureMapTransform transform) { - switch (transform) { - case COPY: return ""; - case TRUNCATE: return " (truncated)"; - //case ESCAPE: - //case CAST: - //case APPEND: - case TRANSLATE: return " (translated)"; - //case DATEOP, - //case UUID, - //case POINTER, - //case EVALUATE, - case CC: return " (--> CodeableConcept)"; - case C: return " (--> Coding)"; - case QTY: return " (--> Quantity)"; - //case ID, - //case CP, - default: - return " {??)"; - } - } - - private PropertyWithType updateProfile(VariableForProfiling var, String element, TypeDetails type, StructureMap map, List profiles, String sliceName, Type fixed, StructureMapGroupRuleTargetComponent tgt) throws FHIRException { - if (var == null) { - assert (Utilities.noString(element)); - // 1. start the new structure definition - StructureDefinition sdn = worker.fetchResource(StructureDefinition.class, type.getType()); - if (sdn == null) - throw new FHIRException("Unable to find definition for "+type.getType()); - ElementDefinition edn = sdn.getSnapshot().getElementFirstRep(); - PropertyWithType pn = createProfile(map, profiles, new PropertyWithType(sdn.getId(), new Property(worker, edn, sdn), null, type), sliceName, tgt); - -// // 2. hook it into the base bundle -// if (type.getType().startsWith("http://hl7.org/fhir/StructureDefinition/") && worker.getResourceNames().contains(type.getType().substring(40))) { -// StructureDefinition sd = var.getProperty().profileProperty.getStructure(); -// ElementDefinition ed = sd.getDifferential().addElement(); -// ed.setPath("Bundle.entry"); -// ed.setName(sliceName); -// ed.setMax("1"); // well, it is for now... -// ed = sd.getDifferential().addElement(); -// ed.setPath("Bundle.entry.fullUrl"); -// ed.setMin(1); -// ed = sd.getDifferential().addElement(); -// ed.setPath("Bundle.entry.resource"); -// ed.setMin(1); -// ed.addType().setCode(pn.getProfileProperty().getStructure().getType()).setProfile(pn.getProfileProperty().getStructure().getUrl()); -// } - return pn; - } else { - assert (!Utilities.noString(element)); - Property pvb = var.getProperty().getBaseProperty(); - Property pvd = var.getProperty().getProfileProperty(); - Property pc = pvb.getChild(element, var.property.types); - if (pc == null) - throw new DefinitionException("Unable to find a definition for "+pvb.getDefinition().getPath()+"."+element); - - // the profile structure definition (derived) - StructureDefinition sd = var.getProperty().profileProperty.getStructure(); - ElementDefinition ednew = sd.getDifferential().addElement(); - ednew.setPath(var.getProperty().profileProperty.getDefinition().getPath()+"."+pc.getName()); - ednew.setUserData("slice-name", sliceName); - ednew.setFixed(fixed); - for (ProfiledType pt : type.getProfiledTypes()) { - if (pt.hasBindings()) - ednew.setBinding(pt.getBindings().get(0)); - if (pt.getUri().startsWith("http://hl7.org/fhir/StructureDefinition/")) { - String t = pt.getUri().substring(40); - t = checkType(t, pc, pt.getProfiles()); - if (t != null) { - if (pt.hasProfiles()) { - for (String p : pt.getProfiles()) - if (t.equals("Reference")) - ednew.addType().setCode(t).setTargetProfile(p); - else - ednew.addType().setCode(t).setProfile(p); - } else - ednew.addType().setCode(t); - } - } - } - - return new PropertyWithType(var.property.path+"."+element, pc, new Property(worker, ednew, sd), type); - } - } - - - - private String checkType(String t, Property pvb, List profiles) throws FHIRException { - if (pvb.getDefinition().getType().size() == 1 && isCompatibleType(t, pvb.getDefinition().getType().get(0).getCode()) && profilesMatch(profiles, pvb.getDefinition().getType().get(0).getProfile())) - return null; - for (TypeRefComponent tr : pvb.getDefinition().getType()) { - if (isCompatibleType(t, tr.getCode())) - return tr.getCode(); // note what is returned - the base type, not the inferred mapping type - } - throw new FHIRException("The type "+t+" is not compatible with the allowed types for "+pvb.getDefinition().getPath()); - } - - private boolean profilesMatch(List profiles, String profile) { - return profiles == null || profiles.size() == 0 || (profiles.size() == 1 && profiles.get(0).equals(profile)); - } - - private boolean isCompatibleType(String t, String code) { - if (t.equals(code)) - return true; - if (t.equals("string")) { - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+code); - if (sd != null && sd.getBaseDefinition().equals("http://hl7.org/fhir/StructureDefinition/string")) - return true; - } - return false; - } - - private TypeDetails analyseTransform(TransformContext context, StructureMap map, StructureMapGroupRuleTargetComponent tgt, VariableForProfiling var, VariablesForProfiling vars) throws FHIRException { - switch (tgt.getTransform()) { - case CREATE : - String p = getParamString(vars, tgt.getParameter().get(0)); - return new TypeDetails(CollectionStatus.SINGLETON, p); - case COPY : - return getParam(vars, tgt.getParameter().get(0)); - case EVALUATE : - ExpressionNode expr = (ExpressionNode) tgt.getUserData(MAP_EXPRESSION); - if (expr == null) { - expr = fpe.parse(getParamString(vars, tgt.getParameter().get(tgt.getParameter().size()-1))); - tgt.setUserData(MAP_WHERE_EXPRESSION, expr); - } - return fpe.check(vars, null, expr); - -////case TRUNCATE : -//// String src = getParamString(vars, tgt.getParameter().get(0)); -//// String len = getParamString(vars, tgt.getParameter().get(1)); -//// if (Utilities.isInteger(len)) { -//// int l = Integer.parseInt(len); -//// if (src.length() > l) -//// src = src.substring(0, l); -//// } -//// return new StringType(src); -////case ESCAPE : -//// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); -////case CAST : -//// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); -////case APPEND : -//// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); - case TRANSLATE : - return new TypeDetails(CollectionStatus.SINGLETON, "CodeableConcept"); - case CC: - ProfiledType res = new ProfiledType("CodeableConcept"); - if (tgt.getParameter().size() >= 2 && isParamId(vars, tgt.getParameter().get(1))) { - TypeDetails td = vars.get(null, getParamId(vars, tgt.getParameter().get(1))).property.types; - if (td != null && td.hasBinding()) - // todo: do we need to check that there's no implicit translation her? I don't think we do... - res.addBinding(td.getBinding()); - } - return new TypeDetails(CollectionStatus.SINGLETON, res); - case C: - return new TypeDetails(CollectionStatus.SINGLETON, "Coding"); - case QTY: - return new TypeDetails(CollectionStatus.SINGLETON, "Quantity"); - case REFERENCE : - VariableForProfiling vrs = vars.get(VariableMode.OUTPUT, getParamId(vars, tgt.getParameterFirstRep())); - if (vrs == null) - throw new FHIRException("Unable to resolve variable \""+getParamId(vars, tgt.getParameterFirstRep())+"\""); - String profile = vrs.property.getProfileProperty().getStructure().getUrl(); - TypeDetails td = new TypeDetails(CollectionStatus.SINGLETON); - td.addType("Reference", profile); - return td; -////case DATEOP : -//// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); -////case UUID : -//// return new IdType(UUID.randomUUID().toString()); -////case POINTER : -//// Base b = getParam(vars, tgt.getParameter().get(0)); -//// if (b instanceof Resource) -//// return new UriType("urn:uuid:"+((Resource) b).getId()); -//// else -//// throw new FHIRException("Transform engine cannot point at an element of type "+b.fhirType()); - default: - throw new Error("Transform Unknown or not handled yet: "+tgt.getTransform().toCode()); - } - } - private String getParamString(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) { - Type p = parameter.getValue(); - if (p == null || p instanceof IdType) - return null; - if (!p.hasPrimitiveValue()) - return null; - return p.primitiveValue(); - } - - private String getParamId(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) { - Type p = parameter.getValue(); - if (p == null || !(p instanceof IdType)) - return null; - return p.primitiveValue(); - } - - private boolean isParamId(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) { - Type p = parameter.getValue(); - if (p == null || !(p instanceof IdType)) - return false; - return vars.get(null, p.primitiveValue()) != null; - } - - private TypeDetails getParam(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) throws DefinitionException { - Type p = parameter.getValue(); - if (!(p instanceof IdType)) - return new TypeDetails(CollectionStatus.SINGLETON, "http://hl7.org/fhir/StructureDefinition/"+p.fhirType()); - else { - String n = ((IdType) p).asStringValue(); - VariableForProfiling b = vars.get(VariableMode.INPUT, n); - if (b == null) - b = vars.get(VariableMode.OUTPUT, n); - if (b == null) - throw new DefinitionException("Variable "+n+" not found ("+vars.summary()+")"); - return b.getProperty().getTypes(); - } - } - - private PropertyWithType createProfile(StructureMap map, List profiles, PropertyWithType prop, String sliceName, Base ctxt) throws DefinitionException { - if (prop.getBaseProperty().getDefinition().getPath().contains(".")) - throw new DefinitionException("Unable to process entry point"); - - String type = prop.getBaseProperty().getDefinition().getPath(); - String suffix = ""; - if (ids.containsKey(type)) { - int id = ids.get(type); - id++; - ids.put(type, id); - suffix = "-"+Integer.toString(id); - } else - ids.put(type, 0); - - StructureDefinition profile = new StructureDefinition(); - profiles.add(profile); - profile.setDerivation(TypeDerivationRule.CONSTRAINT); - profile.setType(type); - profile.setBaseDefinition(prop.getBaseProperty().getStructure().getUrl()); - profile.setName("Profile for "+profile.getType()+" for "+sliceName); - profile.setUrl(map.getUrl().replace("StructureMap", "StructureDefinition")+"-"+profile.getType()+suffix); - ctxt.setUserData("profile", profile.getUrl()); // then we can easily assign this profile url for validation later when we actually transform - profile.setId(map.getId()+"-"+profile.getType()+suffix); - profile.setStatus(map.getStatus()); - profile.setExperimental(map.getExperimental()); - profile.setDescription("Generated automatically from the mapping by the Java Reference Implementation"); - for (ContactDetail c : map.getContact()) { - ContactDetail p = profile.addContact(); - p.setName(c.getName()); - for (ContactPoint cc : c.getTelecom()) - p.addTelecom(cc); - } - profile.setDate(map.getDate()); - profile.setCopyright(map.getCopyright()); - profile.setFhirVersion(Constants.VERSION); - profile.setKind(prop.getBaseProperty().getStructure().getKind()); - profile.setAbstract(false); - ElementDefinition ed = profile.getDifferential().addElement(); - ed.setPath(profile.getType()); - prop.profileProperty = new Property(worker, ed, profile); - return prop; - } - - private PropertyWithType resolveType(StructureMap map, String type, StructureMapInputMode mode) throws Exception { - for (StructureMapStructureComponent imp : map.getStructure()) { - if ((imp.getMode() == StructureMapModelMode.SOURCE && mode == StructureMapInputMode.SOURCE) || - (imp.getMode() == StructureMapModelMode.TARGET && mode == StructureMapInputMode.TARGET)) { - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, imp.getUrl()); - if (sd == null) - throw new Exception("Import "+imp.getUrl()+" cannot be resolved"); - if (sd.getId().equals(type)) { - return new PropertyWithType(sd.getType(), new Property(worker, sd.getSnapshot().getElement().get(0), sd), null, new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl())); - } - } - } - throw new Exception("Unable to find structure definition for "+type+" in imports"); - } - - - public StructureMap generateMapFromMappings(StructureDefinition sd) throws IOException, FHIRException { - String id = getLogicalMappingId(sd); - if (id == null) - return null; - String prefix = ToolingExtensions.readStringExtension(sd, ToolingExtensions.EXT_MAPPING_PREFIX); - String suffix = ToolingExtensions.readStringExtension(sd, ToolingExtensions.EXT_MAPPING_SUFFIX); - if (prefix == null || suffix == null) - return null; - // we build this by text. Any element that has a mapping, we put it's mappings inside it.... - StringBuilder b = new StringBuilder(); - b.append(prefix); - - ElementDefinition root = sd.getSnapshot().getElementFirstRep(); - String m = getMapping(root, id); - if (m != null) - b.append(m+"\r\n"); - addChildMappings(b, id, "", sd, root, false); - b.append("\r\n"); - b.append(suffix); - b.append("\r\n"); - TextFile.stringToFile(b.toString(), "c:\\temp\\test.map"); - StructureMap map = parse(b.toString()); - map.setId(tail(map.getUrl())); - if (!map.hasStatus()) - map.setStatus(PublicationStatus.DRAFT); - map.getText().setStatus(NarrativeStatus.GENERATED); - map.getText().setDiv(new XhtmlNode(NodeType.Element, "div")); - map.getText().getDiv().addTag("pre").addText(render(map)); - return map; - } - - - private String tail(String url) { - return url.substring(url.lastIndexOf("/")+1); - } - - - private void addChildMappings(StringBuilder b, String id, String indent, StructureDefinition sd, ElementDefinition ed, boolean inner) throws DefinitionException { - boolean first = true; - List children = ProfileUtilities.getChildMap(sd, ed); - for (ElementDefinition child : children) { - if (first && inner) { - b.append(" then {\r\n"); - first = false; - } - String map = getMapping(child, id); - if (map != null) { - b.append(indent+" "+child.getPath()+": "+map); - addChildMappings(b, id, indent+" ", sd, child, true); - b.append("\r\n"); - } - } - if (!first && inner) - b.append(indent+"}"); - - } - - - private String getMapping(ElementDefinition ed, String id) { - for (ElementDefinitionMappingComponent map : ed.getMapping()) - if (id.equals(map.getIdentity())) - return map.getMap(); - return null; - } - - - private String getLogicalMappingId(StructureDefinition sd) { - String id = null; - for (StructureDefinitionMappingComponent map : sd.getMapping()) { - if ("http://hl7.org/fhir/logical".equals(map.getUri())) - return map.getIdentity(); - } - return null; - } - -} +package org.hl7.fhir.dstu3.utils; + +// remember group resolution +// trace - account for which wasn't transformed in the source + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.hl7.fhir.dstu3.conformance.ProfileUtilities; +import org.hl7.fhir.dstu3.conformance.ProfileUtilities.ProfileKnowledgeProvider; +import org.hl7.fhir.dstu3.context.IWorkerContext; +import org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult; +import org.hl7.fhir.dstu3.elementmodel.Element; +import org.hl7.fhir.dstu3.elementmodel.Property; +import org.hl7.fhir.dstu3.model.Base; +import org.hl7.fhir.dstu3.model.BooleanType; +import org.hl7.fhir.dstu3.model.CodeType; +import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.ConceptMap; +import org.hl7.fhir.dstu3.model.ConceptMap.ConceptMapGroupComponent; +import org.hl7.fhir.dstu3.model.ConceptMap.SourceElementComponent; +import org.hl7.fhir.dstu3.model.ConceptMap.TargetElementComponent; +import org.hl7.fhir.dstu3.model.Constants; +import org.hl7.fhir.dstu3.model.ContactDetail; +import org.hl7.fhir.dstu3.model.ContactPoint; +import org.hl7.fhir.dstu3.model.DecimalType; +import org.hl7.fhir.dstu3.model.ElementDefinition; +import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionMappingComponent; +import org.hl7.fhir.dstu3.model.ElementDefinition.TypeRefComponent; +import org.hl7.fhir.dstu3.model.Enumeration; +import org.hl7.fhir.dstu3.model.Enumerations.ConceptMapEquivalence; +import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus; +import org.hl7.fhir.dstu3.model.ExpressionNode; +import org.hl7.fhir.dstu3.model.ExpressionNode.CollectionStatus; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.IntegerType; +import org.hl7.fhir.dstu3.model.Narrative.NarrativeStatus; +import org.hl7.fhir.dstu3.model.PrimitiveType; +import org.hl7.fhir.dstu3.model.Reference; +import org.hl7.fhir.dstu3.model.Resource; +import org.hl7.fhir.dstu3.model.ResourceFactory; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.StructureDefinition; +import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionMappingComponent; +import org.hl7.fhir.dstu3.model.StructureDefinition.TypeDerivationRule; +import org.hl7.fhir.dstu3.model.StructureMap; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapContextType; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupComponent; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupInputComponent; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleComponent; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleDependentComponent; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleSourceComponent; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleTargetComponent; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleTargetParameterComponent; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupTypeMode; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapInputMode; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapSourceListMode; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapTargetListMode; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapModelMode; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapStructureComponent; +import org.hl7.fhir.dstu3.model.StructureMap.StructureMapTransform; +import org.hl7.fhir.dstu3.model.Type; +import org.hl7.fhir.dstu3.model.TypeDetails; +import org.hl7.fhir.dstu3.model.TypeDetails.ProfiledType; +import org.hl7.fhir.dstu3.model.UriType; +import org.hl7.fhir.dstu3.model.ValueSet; +import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent; +import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.ValueSetExpansionOutcome; +import org.hl7.fhir.dstu3.utils.FHIRLexer.FHIRLexerException; +import org.hl7.fhir.dstu3.utils.FHIRPathEngine.IEvaluationContext; +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.PathEngineException; +import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.xhtml.NodeType; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; + +/** + * Services in this class: + * + * string render(map) - take a structure and convert it to text + * map parse(text) - take a text representation and parse it + * getTargetType(map) - return the definition for the type to create to hand in + * transform(appInfo, source, map, target) - transform from source to target following the map + * analyse(appInfo, map) - generate profiles and other analysis artifacts for the targets of the transform + * map generateMapFromMappings(StructureDefinition) - build a mapping from a structure definition with loigcal mappings + * + * @author Grahame Grieve + * + */ +public class StructureMapUtilities { + + public class ResolvedGroup { + public StructureMapGroupComponent target; + public StructureMap targetMap; + } + public static final String MAP_WHERE_CHECK = "map.where.check"; + public static final String MAP_WHERE_EXPRESSION = "map.where.expression"; + public static final String MAP_SEARCH_EXPRESSION = "map.search.expression"; + public static final String MAP_EXPRESSION = "map.transform.expression"; + private static final boolean RENDER_MULTIPLE_TARGETS_ONELINE = true; + private static final String AUTO_VAR_NAME = "vvv"; + + public interface ITransformerServices { + // public boolean validateByValueSet(Coding code, String valuesetId); + public void log(String message); // log internal progress + public Base createType(Object appInfo, String name) throws FHIRException; + public Base createResource(Object appInfo, Base res); // an already created resource is provided; this is to identify/store it + public Coding translate(Object appInfo, Coding source, String conceptMapUrl) throws FHIRException; + // public Coding translate(Coding code) + // ValueSet validation operation + // Translation operation + // Lookup another tree of data + // Create an instance tree + // Return the correct string format to refer to a tree (input or output) + public Base resolveReference(Object appContext, String url); + public List performSearch(Object appContext, String url); + } + + private class FFHIRPathHostServices implements IEvaluationContext{ + + public Base resolveConstant(Object appContext, String name) throws PathEngineException { + Variables vars = (Variables) appContext; + Base res = vars.get(VariableMode.INPUT, name); + if (res == null) + res = vars.get(VariableMode.OUTPUT, name); + return res; + } + + @Override + public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { + if (!(appContext instanceof VariablesForProfiling)) + throw new Error("Internal Logic Error (wrong type '"+appContext.getClass().getName()+"' in resolveConstantType)"); + VariablesForProfiling vars = (VariablesForProfiling) appContext; + VariableForProfiling v = vars.get(null, name); + if (v == null) + throw new PathEngineException("Unknown variable '"+name+"' from variables "+vars.summary()); + return v.property.types; + } + + @Override + public boolean log(String argument, List focus) { + throw new Error("Not Implemented Yet"); + } + + @Override + public FunctionDetails resolveFunction(String functionName) { + return null; // throw new Error("Not Implemented Yet"); + } + + @Override + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { + throw new Error("Not Implemented Yet"); + } + + @Override + public List executeFunction(Object appContext, String functionName, List> parameters) { + throw new Error("Not Implemented Yet"); + } + + @Override + public Base resolveReference(Object appContext, String url) { + if (services == null) + return null; + return services.resolveReference(appContext, url); + } + + } + private IWorkerContext worker; + private FHIRPathEngine fpe; + private Map library; + private ITransformerServices services; + private ProfileKnowledgeProvider pkp; + private Map ids = new HashMap(); + + public StructureMapUtilities(IWorkerContext worker, Map library, ITransformerServices services, ProfileKnowledgeProvider pkp) { + super(); + this.worker = worker; + this.library = library; + this.services = services; + this.pkp = pkp; + fpe = new FHIRPathEngine(worker); + fpe.setHostServices(new FFHIRPathHostServices()); + } + + public StructureMapUtilities(IWorkerContext worker, Map library, ITransformerServices services) { + super(); + this.worker = worker; + this.library = library; + this.services = services; + fpe = new FHIRPathEngine(worker); + fpe.setHostServices(new FFHIRPathHostServices()); + } + + public StructureMapUtilities(IWorkerContext worker, Map library) { + super(); + this.worker = worker; + this.library = library; + fpe = new FHIRPathEngine(worker); + fpe.setHostServices(new FFHIRPathHostServices()); + } + + public StructureMapUtilities(IWorkerContext worker) { + super(); + this.worker = worker; + fpe = new FHIRPathEngine(worker); + fpe.setHostServices(new FFHIRPathHostServices()); + } + + public StructureMapUtilities(IWorkerContext worker, ITransformerServices services) { + super(); + this.worker = worker; + this.library = new HashMap(); + for (org.hl7.fhir.dstu3.model.MetadataResource bc : worker.allConformanceResources()) { + if (bc instanceof StructureMap) + library.put(bc.getUrl(), (StructureMap) bc); + } + this.services = services; + fpe = new FHIRPathEngine(worker); + fpe.setHostServices(new FFHIRPathHostServices()); + } + + public static String render(StructureMap map) { + StringBuilder b = new StringBuilder(); + b.append("map \""); + b.append(map.getUrl()); + b.append("\" = \""); + b.append(Utilities.escapeJava(map.getName())); + b.append("\"\r\n\r\n"); + + renderConceptMaps(b, map); + renderUses(b, map); + renderImports(b, map); + for (StructureMapGroupComponent g : map.getGroup()) + renderGroup(b, g); + return b.toString(); + } + + private static void renderConceptMaps(StringBuilder b, StructureMap map) { + for (Resource r : map.getContained()) { + if (r instanceof ConceptMap) { + produceConceptMap(b, (ConceptMap) r); + } + } + } + + private static void produceConceptMap(StringBuilder b, ConceptMap cm) { + b.append("conceptmap \""); + b.append(cm.getId()); + b.append("\" {\r\n"); + Map prefixesSrc = new HashMap(); + Map prefixesTgt = new HashMap(); + char prefix = 's'; + for (ConceptMapGroupComponent cg : cm.getGroup()) { + if (!prefixesSrc.containsKey(cg.getSource())) { + prefixesSrc.put(cg.getSource(), String.valueOf(prefix)); + b.append(" prefix "); + b.append(prefix); + b.append(" = \""); + b.append(cg.getSource()); + b.append("\"\r\n"); + prefix++; + } + if (!prefixesTgt.containsKey(cg.getTarget())) { + prefixesTgt.put(cg.getTarget(), String.valueOf(prefix)); + b.append(" prefix "); + b.append(prefix); + b.append(" = \""); + b.append(cg.getTarget()); + b.append("\"\r\n"); + prefix++; + } + } + b.append("\r\n"); + for (ConceptMapGroupComponent cg : cm.getGroup()) { + for (SourceElementComponent ce : cg.getElement()) { + b.append(" "); + b.append(prefixesSrc.get(cg.getSource())); + b.append(":"); + b.append(ce.getCode()); + b.append(" "); + b.append(getChar(ce.getTargetFirstRep().getEquivalence())); + b.append(" "); + b.append(prefixesTgt.get(cg.getTarget())); + b.append(":"); + b.append(ce.getTargetFirstRep().getCode()); + b.append("\r\n"); + } + } + b.append("}\r\n\r\n"); + } + + private static Object getChar(ConceptMapEquivalence equivalence) { + switch (equivalence) { + case RELATEDTO: return "-"; + case EQUAL: return "="; + case EQUIVALENT: return "=="; + case DISJOINT: return "!="; + case UNMATCHED: return "--"; + case WIDER: return "<="; + case SUBSUMES: return "<-"; + case NARROWER: return ">="; + case SPECIALIZES: return ">-"; + case INEXACT: return "~"; + default: return "??"; + } + } + + private static void renderUses(StringBuilder b, StructureMap map) { + for (StructureMapStructureComponent s : map.getStructure()) { + b.append("uses \""); + b.append(s.getUrl()); + b.append("\" "); + if (s.hasAlias()) { + b.append("alias "); + b.append(s.getAlias()); + b.append(" "); + } + b.append("as "); + b.append(s.getMode().toCode()); + b.append("\r\n"); + renderDoco(b, s.getDocumentation()); + } + if (map.hasStructure()) + b.append("\r\n"); + } + + private static void renderImports(StringBuilder b, StructureMap map) { + for (UriType s : map.getImport()) { + b.append("imports \""); + b.append(s.getValue()); + b.append("\"\r\n"); + } + if (map.hasImport()) + b.append("\r\n"); + } + + public static String groupToString(StructureMapGroupComponent g) { + StringBuilder b = new StringBuilder(); + renderGroup(b, g); + return b.toString(); + } + + private static void renderGroup(StringBuilder b, StructureMapGroupComponent g) { + b.append("group "); + switch (g.getTypeMode()) { + case TYPES: + b.append("for types"); + break; + case TYPEANDTYPES: + b.append("for type+types "); + break; + default: // NONE, NULL + break; + } + b.append(g.getName()); + if (g.hasExtends()) { + b.append(" extends "); + b.append(g.getExtends()); + } + if (g.hasDocumentation()) + renderDoco(b, g.getDocumentation()); + b.append("\r\n"); + for (StructureMapGroupInputComponent gi : g.getInput()) { + b.append(" input "); + b.append(gi.getName()); + if (gi.hasType()) { + b.append(" : "); + b.append(gi.getType()); + } + b.append(" as "); + b.append(gi.getMode().toCode()); + b.append("\r\n"); + } + if (g.hasInput()) + b.append("\r\n"); + for (StructureMapGroupRuleComponent r : g.getRule()) { + renderRule(b, r, 2); + } + b.append("\r\nendgroup\r\n"); + } + + public static String ruleToString(StructureMapGroupRuleComponent r) { + StringBuilder b = new StringBuilder(); + renderRule(b, r, 0); + return b.toString(); + } + + private static void renderRule(StringBuilder b, StructureMapGroupRuleComponent r, int indent) { + for (int i = 0; i < indent; i++) + b.append(' '); + b.append(r.getName()); + b.append(" : for "); + boolean canBeAbbreviated = checkisSimple(r); + + boolean first = true; + for (StructureMapGroupRuleSourceComponent rs : r.getSource()) { + if (first) + first = false; + else + b.append(", "); + renderSource(b, rs, canBeAbbreviated); + } + if (r.getTarget().size() > 1) { + b.append(" make "); + first = true; + for (StructureMapGroupRuleTargetComponent rt : r.getTarget()) { + if (first) + first = false; + else + b.append(", "); + if (RENDER_MULTIPLE_TARGETS_ONELINE) + b.append(' '); + else { + b.append("\r\n"); + for (int i = 0; i < indent+4; i++) + b.append(' '); + } + renderTarget(b, rt, false); + } + } else if (r.hasTarget()) { + b.append(" make "); + renderTarget(b, r.getTarget().get(0), canBeAbbreviated); + } + if (!canBeAbbreviated) { + if (r.hasRule()) { + b.append(" then {\r\n"); + renderDoco(b, r.getDocumentation()); + for (StructureMapGroupRuleComponent ir : r.getRule()) { + renderRule(b, ir, indent+2); + } + for (int i = 0; i < indent; i++) + b.append(' '); + b.append("}\r\n"); + } else { + if (r.hasDependent()) { + b.append(" then "); + first = true; + for (StructureMapGroupRuleDependentComponent rd : r.getDependent()) { + if (first) + first = false; + else + b.append(", "); + b.append(rd.getName()); + b.append("("); + boolean ifirst = true; + for (StringType rdp : rd.getVariable()) { + if (ifirst) + ifirst = false; + else + b.append(", "); + b.append(rdp.asStringValue()); + } + b.append(")"); + } + } + } + } + renderDoco(b, r.getDocumentation()); + b.append("\r\n"); + } + + private static boolean checkisSimple(StructureMapGroupRuleComponent r) { + return + (r.getSource().size() == 1 && r.getSourceFirstRep().hasElement() && r.getSourceFirstRep().hasVariable()) && + (r.getTarget().size() == 1 && r.getTargetFirstRep().hasVariable() && (r.getTargetFirstRep().getTransform() == null || r.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE) && r.getTargetFirstRep().getParameter().size() == 0) && + (r.getDependent().size() == 0); + } + + public static String sourceToString(StructureMapGroupRuleSourceComponent r) { + StringBuilder b = new StringBuilder(); + renderSource(b, r, false); + return b.toString(); + } + + private static void renderSource(StringBuilder b, StructureMapGroupRuleSourceComponent rs, boolean abbreviate) { + b.append(rs.getContext()); + if (rs.getContext().equals("@search")) { + b.append('('); + b.append(rs.getElement()); + b.append(')'); + } else if (rs.hasElement()) { + b.append('.'); + b.append(rs.getElement()); + } + if (rs.hasType()) { + b.append(" : "); + b.append(rs.getType()); + if (rs.hasMin()) { + b.append(" "); + b.append(rs.getMin()); + b.append(".."); + b.append(rs.getMax()); + } + } + + if (rs.hasListMode()) { + b.append(" "); + b.append(rs.getListMode().toCode()); + } + if (rs.hasDefaultValue()) { + b.append(" default "); + assert rs.getDefaultValue() instanceof StringType; + b.append("\""+Utilities.escapeJson(((StringType) rs.getDefaultValue()).asStringValue())+"\""); + } + if (!abbreviate && rs.hasVariable()) { + b.append(" as "); + b.append(rs.getVariable()); + } + if (rs.hasCondition()) { + b.append(" where "); + b.append(rs.getCondition()); + } + if (rs.hasCheck()) { + b.append(" check "); + b.append(rs.getCheck()); + } + } + + public static String targetToString(StructureMapGroupRuleTargetComponent rt) { + StringBuilder b = new StringBuilder(); + renderTarget(b, rt, false); + return b.toString(); + } + + private static void renderTarget(StringBuilder b, StructureMapGroupRuleTargetComponent rt, boolean abbreviate) { + if (rt.hasContext()) { + if (rt.getContextType() == StructureMapContextType.TYPE) + b.append("@"); + b.append(rt.getContext()); + if (rt.hasElement()) { + b.append('.'); + b.append(rt.getElement()); + } + } + if (!abbreviate && rt.hasTransform()) { + if (rt.hasContext()) + b.append(" = "); + if (rt.getTransform() == StructureMapTransform.COPY && rt.getParameter().size() == 1) { + renderTransformParam(b, rt.getParameter().get(0)); + } else if (rt.getTransform() == StructureMapTransform.EVALUATE && rt.getParameter().size() == 1) { + b.append("("); + b.append("\""+((StringType) rt.getParameter().get(0).getValue()).asStringValue()+"\""); + b.append(")"); + } else if (rt.getTransform() == StructureMapTransform.EVALUATE && rt.getParameter().size() == 2) { + b.append(rt.getTransform().toCode()); + b.append("("); + b.append(((IdType) rt.getParameter().get(0).getValue()).asStringValue()); + b.append("\""+((StringType) rt.getParameter().get(1).getValue()).asStringValue()+"\""); + b.append(")"); + } else { + b.append(rt.getTransform().toCode()); + b.append("("); + boolean first = true; + for (StructureMapGroupRuleTargetParameterComponent rtp : rt.getParameter()) { + if (first) + first = false; + else + b.append(", "); + renderTransformParam(b, rtp); + } + b.append(")"); + } + } + if (!abbreviate && rt.hasVariable()) { + b.append(" as "); + b.append(rt.getVariable()); + } + for (Enumeration lm : rt.getListMode()) { + b.append(" "); + b.append(lm.getValue().toCode()); + if (lm.getValue() == StructureMapTargetListMode.SHARE) { + b.append(" "); + b.append(rt.getListRuleId()); + } + } + } + + public static String paramToString(StructureMapGroupRuleTargetParameterComponent rtp) { + StringBuilder b = new StringBuilder(); + renderTransformParam(b, rtp); + return b.toString(); + } + + private static void renderTransformParam(StringBuilder b, StructureMapGroupRuleTargetParameterComponent rtp) { + try { + if (rtp.hasValueBooleanType()) + b.append(rtp.getValueBooleanType().asStringValue()); + else if (rtp.hasValueDecimalType()) + b.append(rtp.getValueDecimalType().asStringValue()); + else if (rtp.hasValueIdType()) + b.append(rtp.getValueIdType().asStringValue()); + else if (rtp.hasValueDecimalType()) + b.append(rtp.getValueDecimalType().asStringValue()); + else if (rtp.hasValueIntegerType()) + b.append(rtp.getValueIntegerType().asStringValue()); + else + b.append("\""+Utilities.escapeJava(rtp.getValueStringType().asStringValue())+"\""); + } catch (FHIRException e) { + e.printStackTrace(); + b.append("error!"); + } + } + + private static void renderDoco(StringBuilder b, String doco) { + if (Utilities.noString(doco)) + return; + b.append(" // "); + b.append(doco.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")); + } + + public StructureMap parse(String text) throws FHIRException { + FHIRLexer lexer = new FHIRLexer(text); + if (lexer.done()) + throw lexer.error("Map Input cannot be empty"); + lexer.skipComments(); + lexer.token("map"); + StructureMap result = new StructureMap(); + result.setUrl(lexer.readConstant("url")); + lexer.token("="); + result.setName(lexer.readConstant("name")); + lexer.skipComments(); + + while (lexer.hasToken("conceptmap")) + parseConceptMap(result, lexer); + + while (lexer.hasToken("uses")) + parseUses(result, lexer); + while (lexer.hasToken("imports")) + parseImports(result, lexer); + + parseGroup(result, lexer); + + while (!lexer.done()) { + parseGroup(result, lexer); + } + + return result; + } + + private void parseConceptMap(StructureMap result, FHIRLexer lexer) throws FHIRLexerException { + lexer.token("conceptmap"); + ConceptMap map = new ConceptMap(); + String id = lexer.readConstant("map id"); + if (!id.startsWith("#")) + lexer.error("Concept Map identifier must start with #"); + map.setId(id); + map.setStatus(PublicationStatus.DRAFT); // todo: how to add this to the text format + result.getContained().add(map); + lexer.token("{"); + lexer.skipComments(); + // lexer.token("source"); + // map.setSource(new UriType(lexer.readConstant("source"))); + // lexer.token("target"); + // map.setSource(new UriType(lexer.readConstant("target"))); + Map prefixes = new HashMap(); + while (lexer.hasToken("prefix")) { + lexer.token("prefix"); + String n = lexer.take(); + lexer.token("="); + String v = lexer.readConstant("prefix url"); + prefixes.put(n, v); + } + while (!lexer.hasToken("}")) { + String srcs = readPrefix(prefixes, lexer); + lexer.token(":"); + String sc = lexer.getCurrent().startsWith("\"") ? lexer.readConstant("code") : lexer.take(); + ConceptMapEquivalence eq = readEquivalence(lexer); + String tgts = (eq != ConceptMapEquivalence.UNMATCHED) ? readPrefix(prefixes, lexer) : ""; + ConceptMapGroupComponent g = getGroup(map, srcs, tgts); + SourceElementComponent e = g.addElement(); + e.setCode(sc); + if (e.getCode().startsWith("\"")) + e.setCode(lexer.processConstant(e.getCode())); + TargetElementComponent tgt = e.addTarget(); + if (eq != ConceptMapEquivalence.EQUIVALENT) + tgt.setEquivalence(eq); + if (tgt.getEquivalence() != ConceptMapEquivalence.UNMATCHED) { + lexer.token(":"); + tgt.setCode(lexer.take()); + if (tgt.getCode().startsWith("\"")) + tgt.setCode(lexer.processConstant(tgt.getCode())); + } + if (lexer.hasComment()) + tgt.setComment(lexer.take().substring(2).trim()); + } + lexer.token("}"); + } + + + private ConceptMapGroupComponent getGroup(ConceptMap map, String srcs, String tgts) { + for (ConceptMapGroupComponent grp : map.getGroup()) { + if (grp.getSource().equals(srcs)) + if ((tgts == null && !grp.hasTarget()) || (tgts != null && tgts.equals(grp.getTarget()))) + return grp; + } + ConceptMapGroupComponent grp = map.addGroup(); + grp.setSource(srcs); + grp.setTarget(tgts); + return grp; + } + + + private String readPrefix(Map prefixes, FHIRLexer lexer) throws FHIRLexerException { + String prefix = lexer.take(); + if (!prefixes.containsKey(prefix)) + throw lexer.error("Unknown prefix '"+prefix+"'"); + return prefixes.get(prefix); + } + + + private ConceptMapEquivalence readEquivalence(FHIRLexer lexer) throws FHIRLexerException { + String token = lexer.take(); + if (token.equals("-")) + return ConceptMapEquivalence.RELATEDTO; + if (token.equals("=")) + return ConceptMapEquivalence.EQUAL; + if (token.equals("==")) + return ConceptMapEquivalence.EQUIVALENT; + if (token.equals("!=")) + return ConceptMapEquivalence.DISJOINT; + if (token.equals("--")) + return ConceptMapEquivalence.UNMATCHED; + if (token.equals("<=")) + return ConceptMapEquivalence.WIDER; + if (token.equals("<-")) + return ConceptMapEquivalence.SUBSUMES; + if (token.equals(">=")) + return ConceptMapEquivalence.NARROWER; + if (token.equals(">-")) + return ConceptMapEquivalence.SPECIALIZES; + if (token.equals("~")) + return ConceptMapEquivalence.INEXACT; + throw lexer.error("Unknown equivalence token '"+token+"'"); + } + + + private void parseUses(StructureMap result, FHIRLexer lexer) throws FHIRException { + lexer.token("uses"); + StructureMapStructureComponent st = result.addStructure(); + st.setUrl(lexer.readConstant("url")); + if (lexer.hasToken("alias")) { + lexer.token("alias"); + st.setAlias(lexer.take()); + } + lexer.token("as"); + st.setMode(StructureMapModelMode.fromCode(lexer.take())); + lexer.skipToken(";"); + if (lexer.hasComment()) { + st.setDocumentation(lexer.take().substring(2).trim()); + } + lexer.skipComments(); + } + + private void parseImports(StructureMap result, FHIRLexer lexer) throws FHIRException { + lexer.token("imports"); + result.addImport(lexer.readConstant("url")); + lexer.skipToken(";"); + if (lexer.hasComment()) { + lexer.next(); + } + lexer.skipComments(); + } + + private void parseGroup(StructureMap result, FHIRLexer lexer) throws FHIRException { + lexer.token("group"); + StructureMapGroupComponent group = result.addGroup(); + if (lexer.hasToken("for")) { + lexer.token("for"); + if ("type".equals(lexer.getCurrent())) { + lexer.token("type"); + lexer.token("+"); + lexer.token("types"); + group.setTypeMode(StructureMapGroupTypeMode.TYPEANDTYPES); + } else { + lexer.token("types"); + group.setTypeMode(StructureMapGroupTypeMode.TYPES); + } + } else + group.setTypeMode(StructureMapGroupTypeMode.NONE); + group.setName(lexer.take()); + if (lexer.hasToken("extends")) { + lexer.next(); + group.setExtends(lexer.take()); + } + lexer.skipComments(); + while (lexer.hasToken("input")) + parseInput(group, lexer); + while (!lexer.hasToken("endgroup")) { + if (lexer.done()) + throw lexer.error("premature termination expecting 'endgroup'"); + parseRule(result, group.getRule(), lexer); + } + lexer.next(); + lexer.skipComments(); + } + + private void parseInput(StructureMapGroupComponent group, FHIRLexer lexer) throws FHIRException { + lexer.token("input"); + StructureMapGroupInputComponent input = group.addInput(); + input.setName(lexer.take()); + if (lexer.hasToken(":")) { + lexer.token(":"); + input.setType(lexer.take()); + } + lexer.token("as"); + input.setMode(StructureMapInputMode.fromCode(lexer.take())); + if (lexer.hasComment()) { + input.setDocumentation(lexer.take().substring(2).trim()); + } + lexer.skipToken(";"); + lexer.skipComments(); + } + + private void parseRule(StructureMap map, List list, FHIRLexer lexer) throws FHIRException { + StructureMapGroupRuleComponent rule = new StructureMapGroupRuleComponent(); + list.add(rule); + rule.setName(lexer.takeDottedToken()); + lexer.token(":"); + lexer.token("for"); + boolean done = false; + while (!done) { + parseSource(rule, lexer); + done = !lexer.hasToken(","); + if (!done) + lexer.next(); + } + if (lexer.hasToken("make")) { + lexer.token("make"); + done = false; + while (!done) { + parseTarget(rule, lexer); + done = !lexer.hasToken(","); + if (!done) + lexer.next(); + } + } + if (lexer.hasToken("then")) { + lexer.token("then"); + if (lexer.hasToken("{")) { + lexer.token("{"); + if (lexer.hasComment()) { + rule.setDocumentation(lexer.take().substring(2).trim()); + } + lexer.skipComments(); + while (!lexer.hasToken("}")) { + if (lexer.done()) + throw lexer.error("premature termination expecting '}' in nested group"); + parseRule(map, rule.getRule(), lexer); + } + lexer.token("}"); + } else { + done = false; + while (!done) { + parseRuleReference(rule, lexer); + done = !lexer.hasToken(","); + if (!done) + lexer.next(); + } + } + } else if (lexer.hasComment()) { + rule.setDocumentation(lexer.take().substring(2).trim()); + } + if (isSimpleSyntax(rule)) { + rule.getSourceFirstRep().setVariable(AUTO_VAR_NAME); + rule.getTargetFirstRep().setVariable(AUTO_VAR_NAME); + rule.getTargetFirstRep().setTransform(StructureMapTransform.CREATE); // with no parameter - e.g. imply what is to be created + // no dependencies - imply what is to be done based on types + } + lexer.skipComments(); + } + + private boolean isSimpleSyntax(StructureMapGroupRuleComponent rule) { + return + (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasContext() && rule.getSourceFirstRep().hasElement() && !rule.getSourceFirstRep().hasVariable()) && + (rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasContext() && rule.getTargetFirstRep().hasElement() && !rule.getTargetFirstRep().hasVariable() && !rule.getTargetFirstRep().hasParameter()) && + (rule.getDependent().size() == 0 && rule.getRule().size() == 0); + } + + private void parseRuleReference(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRLexerException { + StructureMapGroupRuleDependentComponent ref = rule.addDependent(); + ref.setName(lexer.take()); + lexer.token("("); + boolean done = false; + while (!done) { + ref.addVariable(lexer.take()); + done = !lexer.hasToken(","); + if (!done) + lexer.next(); + } + lexer.token(")"); + } + + private void parseSource(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRException { + StructureMapGroupRuleSourceComponent source = rule.addSource(); + source.setContext(lexer.take()); + if (source.getContext().equals("search") && lexer.hasToken("(")) { + source.setContext("@search"); + lexer.take(); + ExpressionNode node = fpe.parse(lexer); + source.setUserData(MAP_SEARCH_EXPRESSION, node); + source.setElement(node.toString()); + lexer.token(")"); + } else if (lexer.hasToken(".")) { + lexer.token("."); + source.setElement(lexer.take()); + } + if (lexer.hasToken(":")) { + // type and cardinality + lexer.token(":"); + source.setType(lexer.takeDottedToken()); + if (!lexer.hasToken("as", "first", "last", "not_first", "not_last", "only_one", "default")) { + source.setMin(lexer.takeInt()); + lexer.token(".."); + source.setMax(lexer.take()); + } + } + if (lexer.hasToken("default")) { + lexer.token("default"); + source.setDefaultValue(new StringType(lexer.readConstant("default value"))); + } + if (Utilities.existsInList(lexer.getCurrent(), "first", "last", "not_first", "not_last", "only_one")) + source.setListMode(StructureMapSourceListMode.fromCode(lexer.take())); + + if (lexer.hasToken("as")) { + lexer.take(); + source.setVariable(lexer.take()); + } + if (lexer.hasToken("where")) { + lexer.take(); + ExpressionNode node = fpe.parse(lexer); + source.setUserData(MAP_WHERE_EXPRESSION, node); + source.setCondition(node.toString()); + } + if (lexer.hasToken("check")) { + lexer.take(); + ExpressionNode node = fpe.parse(lexer); + source.setUserData(MAP_WHERE_CHECK, node); + source.setCheck(node.toString()); + } + } + + private void parseTarget(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRException { + StructureMapGroupRuleTargetComponent target = rule.addTarget(); + String start = lexer.take(); + if (lexer.hasToken(".")) { + target.setContext(start); + target.setContextType(StructureMapContextType.VARIABLE); + start = null; + lexer.token("."); + target.setElement(lexer.take()); + } + String name; + boolean isConstant = false; + if (lexer.hasToken("=")) { + if (start != null) + target.setContext(start); + lexer.token("="); + isConstant = lexer.isConstant(true); + name = lexer.take(); + } else + name = start; + + if ("(".equals(name)) { + // inline fluentpath expression + target.setTransform(StructureMapTransform.EVALUATE); + ExpressionNode node = fpe.parse(lexer); + target.setUserData(MAP_EXPRESSION, node); + target.addParameter().setValue(new StringType(node.toString())); + lexer.token(")"); + } else if (lexer.hasToken("(")) { + target.setTransform(StructureMapTransform.fromCode(name)); + lexer.token("("); + if (target.getTransform() == StructureMapTransform.EVALUATE) { + parseParameter(target, lexer); + lexer.token(","); + ExpressionNode node = fpe.parse(lexer); + target.setUserData(MAP_EXPRESSION, node); + target.addParameter().setValue(new StringType(node.toString())); + } else { + while (!lexer.hasToken(")")) { + parseParameter(target, lexer); + if (!lexer.hasToken(")")) + lexer.token(","); + } + } + lexer.token(")"); + } else if (name != null) { + target.setTransform(StructureMapTransform.COPY); + if (!isConstant) { + String id = name; + while (lexer.hasToken(".")) { + id = id + lexer.take() + lexer.take(); + } + target.addParameter().setValue(new IdType(id)); + } + else + target.addParameter().setValue(readConstant(name, lexer)); + } + if (lexer.hasToken("as")) { + lexer.take(); + target.setVariable(lexer.take()); + } + while (Utilities.existsInList(lexer.getCurrent(), "first", "last", "share", "collate")) { + if (lexer.getCurrent().equals("share")) { + target.addListMode(StructureMapTargetListMode.SHARE); + lexer.next(); + target.setListRuleId(lexer.take()); + } else if (lexer.getCurrent().equals("first")) + target.addListMode(StructureMapTargetListMode.FIRST); + else + target.addListMode(StructureMapTargetListMode.LAST); + lexer.next(); + } + } + + + private void parseParameter(StructureMapGroupRuleTargetComponent target, FHIRLexer lexer) throws FHIRLexerException { + if (!lexer.isConstant(true)) { + target.addParameter().setValue(new IdType(lexer.take())); + } else if (lexer.isStringConstant()) + target.addParameter().setValue(new StringType(lexer.readConstant("??"))); + else { + target.addParameter().setValue(readConstant(lexer.take(), lexer)); + } + } + + private Type readConstant(String s, FHIRLexer lexer) throws FHIRLexerException { + if (Utilities.isInteger(s)) + return new IntegerType(s); + else if (Utilities.isDecimal(s)) + return new DecimalType(s); + else if (Utilities.existsInList(s, "true", "false")) + return new BooleanType(s.equals("true")); + else + return new StringType(lexer.processConstant(s)); + } + + public StructureDefinition getTargetType(StructureMap map) throws FHIRException { + boolean found = false; + StructureDefinition res = null; + for (StructureMapStructureComponent uses : map.getStructure()) { + if (uses.getMode() == StructureMapModelMode.TARGET) { + if (found) + throw new FHIRException("Multiple targets found in map "+map.getUrl()); + found = true; + res = worker.fetchResource(StructureDefinition.class, uses.getUrl()); + if (res == null) + throw new FHIRException("Unable to find "+uses.getUrl()+" referenced from map "+map.getUrl()); + } + } + if (res == null) + throw new FHIRException("No targets found in map "+map.getUrl()); + return res; + } + + public enum VariableMode { + INPUT, OUTPUT + } + + public class Variable { + private VariableMode mode; + private String name; + private Base object; + public Variable(VariableMode mode, String name, Base object) { + super(); + this.mode = mode; + this.name = name; + this.object = object; + } + public VariableMode getMode() { + return mode; + } + public String getName() { + return name; + } + public Base getObject() { + return object; + } + public String summary() { + return name+": "+object.fhirType(); + } + } + + public class Variables { + private List list = new ArrayList(); + + public void add(VariableMode mode, String name, Base object) { + Variable vv = null; + for (Variable v : list) + if ((v.mode == mode) && v.getName().equals(name)) + vv = v; + if (vv != null) + list.remove(vv); + list.add(new Variable(mode, name, object)); + } + + public Variables copy() { + Variables result = new Variables(); + result.list.addAll(list); + return result; + } + + public Base get(VariableMode mode, String name) { + for (Variable v : list) + if ((v.mode == mode) && v.getName().equals(name)) + return v.getObject(); + return null; + } + + public String summary() { + CommaSeparatedStringBuilder s = new CommaSeparatedStringBuilder(); + CommaSeparatedStringBuilder t = new CommaSeparatedStringBuilder(); + for (Variable v : list) + if (v.mode == VariableMode.INPUT) + s.append(v.summary()); + else + t.append(v.summary()); + return "source variables ["+s.toString()+"], target variables ["+t.toString()+"]"; + } + } + + public class TransformContext { + private Object appInfo; + + public TransformContext(Object appInfo) { + super(); + this.appInfo = appInfo; + } + + public Object getAppInfo() { + return appInfo; + } + + } + + private void log(String cnt) { + if (services != null) + services.log(cnt); + } + + /** + * 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 descendents + * + * @param item + * @param name + * @param result + * @throws FHIRException + */ + protected void getChildrenByName(Base item, String name, List result) throws FHIRException { + for (Base v : item.listChildrenByName(name, true)) + if (v != null) + result.add(v); + } + + public void transform(Object appInfo, Base source, StructureMap map, Base target) throws FHIRException { + TransformContext context = new TransformContext(appInfo); + log("Start Transform "+map.getUrl()); + StructureMapGroupComponent g = map.getGroup().get(0); + + Variables vars = new Variables(); + vars.add(VariableMode.INPUT, getInputName(g, StructureMapInputMode.SOURCE, "source"), source); + vars.add(VariableMode.OUTPUT, getInputName(g, StructureMapInputMode.TARGET, "target"), target); + + executeGroup("", context, map, vars, g); + if (target instanceof Element) + ((Element) target).sort(); + } + + private String getInputName(StructureMapGroupComponent g, StructureMapInputMode mode, String def) throws DefinitionException { + String name = null; + for (StructureMapGroupInputComponent inp : g.getInput()) { + if (inp.getMode() == mode) + if (name != null) + throw new DefinitionException("This engine does not support multiple source inputs"); + else + name = inp.getName(); + } + return name == null ? def : name; + } + + private void executeGroup(String indent, TransformContext context, StructureMap map, Variables vars, StructureMapGroupComponent group) throws FHIRException { + log(indent+"Group : "+group.getName()); + // todo: check inputs + if (group.hasExtends()) { + ResolvedGroup rg = resolveGroupReference(map, group, group.getExtends()); + executeGroup(indent+" ", context, rg.targetMap, vars, rg.target); + } + + for (StructureMapGroupRuleComponent r : group.getRule()) { + executeRule(indent+" ", context, map, vars, group, r); + } + } + + private void executeRule(String indent, TransformContext context, StructureMap map, Variables vars, StructureMapGroupComponent group, StructureMapGroupRuleComponent rule) throws FHIRException { + log(indent+"rule : "+rule.getName()); + if (rule.getName().contains("CarePlan.participant-unlink")) + System.out.println("debug"); + Variables srcVars = vars.copy(); + if (rule.getSource().size() != 1) + throw new FHIRException("Rule \""+rule.getName()+"\": not handled yet"); + List source = processSource(rule.getName(), context, srcVars, rule.getSource().get(0)); + if (source != null) { + for (Variables v : source) { + for (StructureMapGroupRuleTargetComponent t : rule.getTarget()) { + processTarget(rule.getName(), context, v, map, group, t, rule.getSource().size() == 1 ? rule.getSourceFirstRep().getVariable() : null); + } + if (rule.hasRule()) { + for (StructureMapGroupRuleComponent childrule : rule.getRule()) { + executeRule(indent +" ", context, map, v, group, childrule); + } + } else if (rule.hasDependent()) { + for (StructureMapGroupRuleDependentComponent dependent : rule.getDependent()) { + executeDependency(indent+" ", context, map, v, group, dependent); + } + } else if (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasVariable() && rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasVariable() && rule.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE && !rule.getTargetFirstRep().hasParameter()) { + // simple inferred, map by type + Base src = v.get(VariableMode.INPUT, rule.getSourceFirstRep().getVariable()); + Base tgt = v.get(VariableMode.OUTPUT, rule.getTargetFirstRep().getVariable()); + String srcType = src.fhirType(); + String tgtType = tgt.fhirType(); + ResolvedGroup defGroup = resolveGroupByTypes(map, rule.getName(), group, srcType, tgtType); + Variables vdef = new Variables(); + vdef.add(VariableMode.INPUT, defGroup.target.getInput().get(0).getName(), src); + vdef.add(VariableMode.OUTPUT, defGroup.target.getInput().get(1).getName(), tgt); + executeGroup(indent+" ", context, defGroup.targetMap, vdef, defGroup.target); + } + } + } + } + + private void executeDependency(String indent, TransformContext context, StructureMap map, Variables vin, StructureMapGroupComponent group, StructureMapGroupRuleDependentComponent dependent) throws FHIRException { + ResolvedGroup rg = resolveGroupReference(map, group, dependent.getName()); + + if (rg.target.getInput().size() != dependent.getVariable().size()) { + throw new FHIRException("Rule '"+dependent.getName()+"' has "+Integer.toString(rg.target.getInput().size())+" but the invocation has "+Integer.toString(dependent.getVariable().size())+" variables"); + } + Variables v = new Variables(); + for (int i = 0; i < rg.target.getInput().size(); i++) { + StructureMapGroupInputComponent input = rg.target.getInput().get(i); + StringType rdp = dependent.getVariable().get(i); + String var = rdp.asStringValue(); + VariableMode mode = input.getMode() == StructureMapInputMode.SOURCE ? VariableMode.INPUT : VariableMode.OUTPUT; + Base vv = vin.get(mode, var); + if (vv == null && mode == VariableMode.INPUT) //* once source, always source. but target can be treated as source at user convenient + vv = vin.get(VariableMode.OUTPUT, var); + if (vv == null) + throw new FHIRException("Rule '"+dependent.getName()+"' "+mode.toString()+" variable '"+input.getName()+"' named as '"+var+"' has no value"); + v.add(mode, input.getName(), vv); + } + executeGroup(indent+" ", context, rg.targetMap, v, rg.target); + } + + private String determineTypeFromSourceType(StructureMap map, StructureMapGroupComponent source, Base base, String[] types) throws FHIRException { + String type = base.fhirType(); + String kn = "type^"+type; + if (source.hasUserData(kn)) + return source.getUserString(kn); + + ResolvedGroup res = new ResolvedGroup(); + res.targetMap = null; + res.target = null; + for (StructureMapGroupComponent grp : map.getGroup()) { + if (matchesByType(map, grp, type)) { + if (res.targetMap == null) { + res.targetMap = map; + res.target = grp; + } else + throw new FHIRException("Multiple possible matches looking for default rule for '"+type+"'"); + } + } + if (res.targetMap != null) { + String result = getActualType(res.targetMap, res.target.getInput().get(1).getType()); + source.setUserData(kn, result); + return result; + } + + for (UriType imp : map.getImport()) { + List impMapList = findMatchingMaps(imp.getValue()); + if (impMapList.size() == 0) + throw new FHIRException("Unable to find map(s) for "+imp.getValue()); + for (StructureMap impMap : impMapList) { + if (!impMap.getUrl().equals(map.getUrl())) { + for (StructureMapGroupComponent grp : impMap.getGroup()) { + if (matchesByType(impMap, grp, type)) { + if (res.targetMap == null) { + res.targetMap = impMap; + res.target = grp; + } else + throw new FHIRException("Multiple possible matches for default rule for '"+type+"' in "+res.targetMap.getUrl()+" ("+res.target.getName()+") and "+impMap.getUrl()+" ("+grp.getName()+")"); + } + } + } + } + } + if (res.target == null) + throw new FHIRException("No matches found for default rule for '"+type+"' from "+map.getUrl()); + String result = getActualType(res.targetMap, res.target.getInput().get(1).getType()); // should be .getType, but R2... + source.setUserData(kn, result); + return result; + } + + private List findMatchingMaps(String value) { + List res = new ArrayList(); + if (value.contains("*")) { + for (StructureMap sm : library.values()) { + if (urlMatches(value, sm.getUrl())) { + res.add(sm); + } + } + } else { + StructureMap sm = library.get(value); + if (sm != null) + res.add(sm); + } + Set check = new HashSet(); + for (StructureMap sm : res) { + if (check.contains(sm.getUrl())) + throw new Error("duplicate"); + else + check.add(sm.getUrl()); + } + return res; + } + + private boolean urlMatches(String mask, String url) { + return url.length() > mask.length() && url.startsWith(mask.substring(0, mask.indexOf("*"))) && url.endsWith(mask.substring(mask.indexOf("*")+1)) ; + } + + private ResolvedGroup resolveGroupByTypes(StructureMap map, String ruleid, StructureMapGroupComponent source, String srcType, String tgtType) throws FHIRException { + String kn = "types^"+srcType+":"+tgtType; + if (source.hasUserData(kn)) + return (ResolvedGroup) source.getUserData(kn); + + ResolvedGroup res = new ResolvedGroup(); + res.targetMap = null; + res.target = null; + for (StructureMapGroupComponent grp : map.getGroup()) { + if (matchesByType(map, grp, srcType, tgtType)) { + if (res.targetMap == null) { + res.targetMap = map; + res.target = grp; + } else + throw new FHIRException("Multiple possible matches looking for rule for '"+srcType+"/"+tgtType+"', from rule '"+ruleid+"'"); + } + } + if (res.targetMap != null) { + source.setUserData(kn, res); + return res; + } + + for (UriType imp : map.getImport()) { + List impMapList = findMatchingMaps(imp.getValue()); + if (impMapList.size() == 0) + throw new FHIRException("Unable to find map(s) for "+imp.getValue()); + for (StructureMap impMap : impMapList) { + if (!impMap.getUrl().equals(map.getUrl())) { + for (StructureMapGroupComponent grp : impMap.getGroup()) { + if (matchesByType(impMap, grp, srcType, tgtType)) { + if (res.targetMap == null) { + res.targetMap = impMap; + res.target = grp; + } else + throw new FHIRException("Multiple possible matches for rule for '"+srcType+"/"+tgtType+"' in "+res.targetMap.getUrl()+" and "+impMap.getUrl()+", from rule '"+ruleid+"'"); + } + } + } + } + } + if (res.target == null) + throw new FHIRException("No matches found for rule for '"+srcType+"/"+tgtType+"' from "+map.getUrl()+", from rule '"+ruleid+"'"); + source.setUserData(kn, res); + return res; + } + + + private boolean matchesByType(StructureMap map, StructureMapGroupComponent grp, String type) throws FHIRException { + if (grp.getTypeMode() != StructureMapGroupTypeMode.TYPEANDTYPES) + return false; + if (grp.getInput().size() != 2 || grp.getInput().get(0).getMode() != StructureMapInputMode.SOURCE || grp.getInput().get(1).getMode() != StructureMapInputMode.TARGET) + return false; + return matchesType(map, type, grp.getInput().get(0).getType()); + } + + private boolean matchesByType(StructureMap map, StructureMapGroupComponent grp, String srcType, String tgtType) throws FHIRException { + if (grp.getTypeMode() == StructureMapGroupTypeMode.NONE) + return false; + if (grp.getInput().size() != 2 || grp.getInput().get(0).getMode() != StructureMapInputMode.SOURCE || grp.getInput().get(1).getMode() != StructureMapInputMode.TARGET) + return false; + if (!grp.getInput().get(0).hasType() || !grp.getInput().get(1).hasType()) + return false; + return matchesType(map, srcType, grp.getInput().get(0).getType()) && matchesType(map, tgtType, grp.getInput().get(1).getType()); + } + + private boolean matchesType(StructureMap map, String actualType, String statedType) throws FHIRException { + // check the aliases + for (StructureMapStructureComponent imp : map.getStructure()) { + if (imp.hasAlias() && statedType.equals(imp.getAlias())) { + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, imp.getUrl()); + if (sd != null) + statedType = sd.getType(); + break; + } + } + + return actualType.equals(statedType); + } + + private String getActualType(StructureMap map, String statedType) throws FHIRException { + // check the aliases + for (StructureMapStructureComponent imp : map.getStructure()) { + if (imp.hasAlias() && statedType.equals(imp.getAlias())) { + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, imp.getUrl()); + if (sd == null) + throw new FHIRException("Unable to resolve structure "+imp.getUrl()); + return sd.getId(); // should be sd.getType(), but R2... + } + } + return statedType; + } + + + private ResolvedGroup resolveGroupReference(StructureMap map, StructureMapGroupComponent source, String name) throws FHIRException { + String kn = "ref^"+name; + if (source.hasUserData(kn)) + return (ResolvedGroup) source.getUserData(kn); + + ResolvedGroup res = new ResolvedGroup(); + res.targetMap = null; + res.target = null; + for (StructureMapGroupComponent grp : map.getGroup()) { + if (grp.getName().equals(name)) { + if (res.targetMap == null) { + res.targetMap = map; + res.target = grp; + } else + throw new FHIRException("Multiple possible matches for rule '"+name+"'"); + } + } + if (res.targetMap != null) { + source.setUserData(kn, res); + return res; + } + + for (UriType imp : map.getImport()) { + List impMapList = findMatchingMaps(imp.getValue()); + if (impMapList.size() == 0) + throw new FHIRException("Unable to find map(s) for "+imp.getValue()); + for (StructureMap impMap : impMapList) { + if (!impMap.getUrl().equals(map.getUrl())) { + for (StructureMapGroupComponent grp : impMap.getGroup()) { + if (grp.getName().equals(name)) { + if (res.targetMap == null) { + res.targetMap = impMap; + res.target = grp; + } else + throw new FHIRException("Multiple possible matches for rule '"+name+"' in "+res.targetMap.getUrl()+" and "+impMap.getUrl()); + } + } + } + } + } + if (res.target == null) + throw new FHIRException("No matches found for rule '"+name+"'. Reference found in "+map.getUrl()); + source.setUserData(kn, res); + return res; + } + + private List processSource(String ruleId, TransformContext context, Variables vars, StructureMapGroupRuleSourceComponent src) throws FHIRException { + List items; + if (src.getContext().equals("@search")) { + ExpressionNode expr = (ExpressionNode) src.getUserData(MAP_SEARCH_EXPRESSION); + if (expr == null) { + expr = fpe.parse(src.getElement()); + src.setUserData(MAP_SEARCH_EXPRESSION, expr); + } + String search = fpe.evaluateToString(vars, null, new StringType(), expr); // string is a holder of nothing to ensure that variables are processed correctly + items = services.performSearch(context.appInfo, search); + } else { + items = new ArrayList(); + Base b = vars.get(VariableMode.INPUT, src.getContext()); + if (b == null) + throw new FHIRException("Unknown input variable "+src.getContext()); + + if (!src.hasElement()) + items.add(b); + else { + getChildrenByName(b, src.getElement(), items); + if (items.size() == 0 && src.hasDefaultValue()) + items.add(src.getDefaultValue()); + } + } + + if (src.hasType()) { + List remove = new ArrayList(); + for (Base item : items) { + if (item != null && !isType(item, src.getType())) { + remove.add(item); + } + } + items.removeAll(remove); + } + + if (src.hasCondition()) { + ExpressionNode expr = (ExpressionNode) src.getUserData(MAP_WHERE_EXPRESSION); + if (expr == null) { + expr = fpe.parse(src.getCondition()); + // fpe.check(context.appInfo, ??, ??, expr) + src.setUserData(MAP_WHERE_EXPRESSION, expr); + } + List remove = new ArrayList(); + for (Base item : items) { + if (!fpe.evaluateToBoolean(vars, null, item, expr)) + remove.add(item); + } + items.removeAll(remove); + } + + if (src.hasCheck()) { + ExpressionNode expr = (ExpressionNode) src.getUserData(MAP_WHERE_CHECK); + if (expr == null) { + expr = fpe.parse(src.getCheck()); + // fpe.check(context.appInfo, ??, ??, expr) + src.setUserData(MAP_WHERE_CHECK, expr); + } + List remove = new ArrayList(); + for (Base item : items) { + if (!fpe.evaluateToBoolean(vars, null, item, expr)) + throw new FHIRException("Rule \""+ruleId+"\": Check condition failed"); + } + } + + + if (src.hasListMode() && !items.isEmpty()) { + switch (src.getListMode()) { + case FIRST: + Base bt = items.get(0); + items.clear(); + items.add(bt); + break; + case NOTFIRST: + if (items.size() > 0) + items.remove(0); + break; + case LAST: + bt = items.get(items.size()-1); + items.clear(); + items.add(bt); + break; + case NOTLAST: + if (items.size() > 0) + items.remove(items.size()-1); + break; + case ONLYONE: + if (items.size() > 1) + throw new FHIRException("Rule \""+ruleId+"\": Check condition failed: the collection has more than one item"); + break; + case NULL: + } + } + List result = new ArrayList(); + for (Base r : items) { + Variables v = vars.copy(); + if (src.hasVariable()) + v.add(VariableMode.INPUT, src.getVariable(), r); + result.add(v); + } + return result; + } + + + private boolean isType(Base item, String type) { + if (type.equals(item.fhirType())) + return true; + return false; + } + + private void processTarget(String ruleId, TransformContext context, Variables vars, StructureMap map, StructureMapGroupComponent group, StructureMapGroupRuleTargetComponent tgt, String srcVar) throws FHIRException { + Base dest = null; + if (tgt.hasContext()) { + dest = vars.get(VariableMode.OUTPUT, tgt.getContext()); + if (dest == null) + throw new FHIRException("Rule \""+ruleId+"\": target context not known: "+tgt.getContext()); + if (!tgt.hasElement()) + throw new FHIRException("Rule \""+ruleId+"\": Not supported yet"); + } + Base v = null; + if (tgt.hasTransform()) { + v = runTransform(ruleId, context, map, group, tgt, vars, dest, tgt.getElement(), srcVar); + if (v != null && dest != null) + v = dest.setProperty(tgt.getElement().hashCode(), tgt.getElement(), v); // reset v because some implementations may have to rewrite v when setting the value + } else if (dest != null) + v = dest.makeProperty(tgt.getElement().hashCode(), tgt.getElement()); + if (tgt.hasVariable() && v != null) + vars.add(VariableMode.OUTPUT, tgt.getVariable(), v); + } + + private Base runTransform(String ruleId, TransformContext context, StructureMap map, StructureMapGroupComponent group, StructureMapGroupRuleTargetComponent tgt, Variables vars, Base dest, String element, String srcVar) throws FHIRException { + try { + switch (tgt.getTransform()) { + case CREATE : + String tn; + if (tgt.getParameter().isEmpty()) { + // we have to work out the type. First, we see if there is a single type for the target. If there is, we use that + String[] types = dest.getTypesForProperty(element.hashCode(), element); + if (types.length == 1 && !"*".equals(types[0]) && !types[0].equals("Resource")) + tn = types[0]; + else if (srcVar != null) { + tn = determineTypeFromSourceType(map, group, vars.get(VariableMode.INPUT, srcVar), types); + } else + throw new Error("Cannot determine type implicitly because there is no single input variable"); + } else + tn = getParamStringNoNull(vars, tgt.getParameter().get(0), tgt.toString()); + Base res = services != null ? services.createType(context.getAppInfo(), tn) : ResourceFactory.createResourceOrType(tn); + if (res.isResource() && !res.fhirType().equals("Parameters")) { +// res.setIdBase(tgt.getParameter().size() > 1 ? getParamString(vars, tgt.getParameter().get(0)) : UUID.randomUUID().toString().toLowerCase()); + if (services != null) + res = services.createResource(context.getAppInfo(), res); + } + if (tgt.hasUserData("profile")) + res.setUserData("profile", tgt.getUserData("profile")); + return res; + case COPY : + return getParam(vars, tgt.getParameter().get(0)); + case EVALUATE : + ExpressionNode expr = (ExpressionNode) tgt.getUserData(MAP_EXPRESSION); + if (expr == null) { + expr = fpe.parse(getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString())); + tgt.setUserData(MAP_WHERE_EXPRESSION, expr); + } + List v = fpe.evaluate(vars, null, tgt.getParameter().size() == 2 ? getParam(vars, tgt.getParameter().get(0)) : new BooleanType(false), expr); + if (v.size() == 0) + return null; + else if (v.size() != 1) + throw new FHIRException("Rule \""+ruleId+"\": Evaluation of "+expr.toString()+" returned "+Integer.toString(v.size())+" objects"); + else + return v.get(0); + + case TRUNCATE : + String src = getParamString(vars, tgt.getParameter().get(0)); + String len = getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString()); + if (Utilities.isInteger(len)) { + int l = Integer.parseInt(len); + if (src.length() > l) + src = src.substring(0, l); + } + return new StringType(src); + case ESCAPE : + throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); + case CAST : + throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); + case APPEND : + throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); + case TRANSLATE : + return translate(context, map, vars, tgt.getParameter()); + case REFERENCE : + Base b = getParam(vars, tgt.getParameter().get(0)); + if (b == null) + throw new FHIRException("Rule \""+ruleId+"\": Unable to find parameter "+((IdType) tgt.getParameter().get(0).getValue()).asStringValue()); + if (!b.isResource()) + throw new FHIRException("Rule \""+ruleId+"\": Transform engine cannot point at an element of type "+b.fhirType()); + else { + String id = b.getIdBase(); + if (id == null) { + id = UUID.randomUUID().toString().toLowerCase(); + b.setIdBase(id); + } + return new Reference().setReference(b.fhirType()+"/"+id); + } + case DATEOP : + throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); + case UUID : + return new IdType(UUID.randomUUID().toString()); + case POINTER : + b = getParam(vars, tgt.getParameter().get(0)); + if (b instanceof Resource) + return new UriType("urn:uuid:"+((Resource) b).getId()); + else + throw new FHIRException("Rule \""+ruleId+"\": Transform engine cannot point at an element of type "+b.fhirType()); + case CC: + CodeableConcept cc = new CodeableConcept(); + cc.addCoding(buildCoding(getParamStringNoNull(vars, tgt.getParameter().get(0), tgt.toString()), getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString()))); + return cc; + case C: + Coding c = buildCoding(getParamStringNoNull(vars, tgt.getParameter().get(0), tgt.toString()), getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString())); + return c; + default: + throw new Error("Rule \""+ruleId+"\": Transform Unknown: "+tgt.getTransform().toCode()); + } + } catch (Exception e) { + throw new FHIRException("Exception executing transform "+tgt.toString()+" on Rule \""+ruleId+"\": "+e.getMessage(), e); + } + } + + + private Coding buildCoding(String uri, String code) throws FHIRException { + // if we can get this as a valueSet, we will + String system = null; + String display = null; + ValueSet vs = Utilities.noString(uri) ? null : worker.fetchResourceWithException(ValueSet.class, uri); + if (vs != null) { + ValueSetExpansionOutcome vse = worker.expandVS(vs, true, false); + if (vse.getError() != null) + throw new FHIRException(vse.getError()); + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (ValueSetExpansionContainsComponent t : vse.getValueset().getExpansion().getContains()) { + if (t.hasCode()) + b.append(t.getCode()); + if (code.equals(t.getCode()) && t.hasSystem()) { + system = t.getSystem(); + display = t.getDisplay(); + break; + } + if (code.equalsIgnoreCase(t.getDisplay()) && t.hasSystem()) { + system = t.getSystem(); + display = t.getDisplay(); + break; + } + } + if (system == null) + throw new FHIRException("The code '"+code+"' is not in the value set '"+uri+"' (valid codes: "+b.toString()+"; also checked displays)"); + } else + system = uri; + ValidationResult vr = worker.validateCode(system, code, null); + if (vr != null && vr.getDisplay() != null) + display = vr.getDisplay(); + return new Coding().setSystem(system).setCode(code).setDisplay(display); + } + + + private String getParamStringNoNull(Variables vars, StructureMapGroupRuleTargetParameterComponent parameter, String message) throws FHIRException { + Base b = getParam(vars, parameter); + if (b == null) + throw new FHIRException("Unable to find a value for "+parameter.toString()+". Context: "+message); + if (!b.hasPrimitiveValue()) + throw new FHIRException("Found a value for "+parameter.toString()+", but it has a type of "+b.fhirType()+" and cannot be treated as a string. Context: "+message); + return b.primitiveValue(); + } + + private String getParamString(Variables vars, StructureMapGroupRuleTargetParameterComponent parameter) throws DefinitionException { + Base b = getParam(vars, parameter); + if (b == null || !b.hasPrimitiveValue()) + return null; + return b.primitiveValue(); + } + + + private Base getParam(Variables vars, StructureMapGroupRuleTargetParameterComponent parameter) throws DefinitionException { + Type p = parameter.getValue(); + if (!(p instanceof IdType)) + return p; + else { + String n = ((IdType) p).asStringValue(); + Base b = vars.get(VariableMode.INPUT, n); + if (b == null) + b = vars.get(VariableMode.OUTPUT, n); + if (b == null) + throw new DefinitionException("Variable "+n+" not found ("+vars.summary()+")"); + return b; + } + } + + + private Base translate(TransformContext context, StructureMap map, Variables vars, List parameter) throws FHIRException { + Base src = getParam(vars, parameter.get(0)); + String id = getParamString(vars, parameter.get(1)); + String fld = parameter.size() > 2 ? getParamString(vars, parameter.get(2)) : null; + return translate(context, map, src, id, fld); + } + + private class SourceElementComponentWrapper { + private ConceptMapGroupComponent group; + private SourceElementComponent comp; + public SourceElementComponentWrapper(ConceptMapGroupComponent group, SourceElementComponent comp) { + super(); + this.group = group; + this.comp = comp; + } + } + public Base translate(TransformContext context, StructureMap map, Base source, String conceptMapUrl, String fieldToReturn) throws FHIRException { + Coding src = new Coding(); + if (source.isPrimitive()) { + src.setCode(source.primitiveValue()); + } else if ("Coding".equals(source.fhirType())) { + Base[] b = source.getProperty("system".hashCode(), "system", true); + if (b.length == 1) + src.setSystem(b[0].primitiveValue()); + b = source.getProperty("code".hashCode(), "code", true); + if (b.length == 1) + src.setCode(b[0].primitiveValue()); + } else if ("CE".equals(source.fhirType())) { + Base[] b = source.getProperty("codeSystem".hashCode(), "codeSystem", true); + if (b.length == 1) + src.setSystem(b[0].primitiveValue()); + b = source.getProperty("code".hashCode(), "code", true); + if (b.length == 1) + src.setCode(b[0].primitiveValue()); + } else + throw new FHIRException("Unable to translate source "+source.fhirType()); + + String su = conceptMapUrl; + if (conceptMapUrl.equals("http://hl7.org/fhir/ConceptMap/special-oid2uri")) { + String uri = worker.oid2Uri(src.getCode()); + if (uri == null) + uri = "urn:oid:"+src.getCode(); + if ("uri".equals(fieldToReturn)) + return new UriType(uri); + else + throw new FHIRException("Error in return code"); + } else { + ConceptMap cmap = null; + if (conceptMapUrl.startsWith("#")) { + for (Resource r : map.getContained()) { + if (r instanceof ConceptMap && ((ConceptMap) r).getId().equals(conceptMapUrl.substring(1))) { + cmap = (ConceptMap) r; + su = map.getUrl()+conceptMapUrl; + } + } + if (cmap == null) + throw new FHIRException("Unable to translate - cannot find map "+conceptMapUrl); + } else + cmap = worker.fetchResource(ConceptMap.class, conceptMapUrl); + Coding outcome = null; + boolean done = false; + String message = null; + if (cmap == null) { + if (services == null) + message = "No map found for "+conceptMapUrl; + else { + outcome = services.translate(context.appInfo, src, conceptMapUrl); + done = true; + } + } else { + List list = new ArrayList(); + for (ConceptMapGroupComponent g : cmap.getGroup()) { + for (SourceElementComponent e : g.getElement()) { + if (!src.hasSystem() && src.getCode().equals(e.getCode())) + list.add(new SourceElementComponentWrapper(g, e)); + else if (src.hasSystem() && src.getSystem().equals(g.getSource()) && src.getCode().equals(e.getCode())) + list.add(new SourceElementComponentWrapper(g, e)); + } + } + if (list.size() == 0) + done = true; + else if (list.get(0).comp.getTarget().size() == 0) + message = "Concept map "+su+" found no translation for "+src.getCode(); + else { + for (TargetElementComponent tgt : list.get(0).comp.getTarget()) { + if (tgt.getEquivalence() == null || EnumSet.of( ConceptMapEquivalence.EQUAL , ConceptMapEquivalence.RELATEDTO , ConceptMapEquivalence.EQUIVALENT, ConceptMapEquivalence.WIDER).contains(tgt.getEquivalence())) { + if (done) { + message = "Concept map "+su+" found multiple matches for "+src.getCode(); + done = false; + } else { + done = true; + outcome = new Coding().setCode(tgt.getCode()).setSystem(list.get(0).group.getTarget()); + } + } else if (tgt.getEquivalence() == ConceptMapEquivalence.UNMATCHED) { + done = true; + } + } + if (!done) + message = "Concept map "+su+" found no usable translation for "+src.getCode(); + } + } + if (!done) + throw new FHIRException(message); + if (outcome == null) + return null; + if ("code".equals(fieldToReturn)) + return new CodeType(outcome.getCode()); + else + return outcome; + } + } + + + public Map getLibrary() { + return library; + } + + public class PropertyWithType { + private String path; + private Property baseProperty; + private Property profileProperty; + private TypeDetails types; + public PropertyWithType(String path, Property baseProperty, Property profileProperty, TypeDetails types) { + super(); + this.baseProperty = baseProperty; + this.profileProperty = profileProperty; + this.path = path; + this.types = types; + } + + public TypeDetails getTypes() { + return types; + } + public String getPath() { + return path; + } + + public Property getBaseProperty() { + return baseProperty; + } + + public void setBaseProperty(Property baseProperty) { + this.baseProperty = baseProperty; + } + + public Property getProfileProperty() { + return profileProperty; + } + + public void setProfileProperty(Property profileProperty) { + this.profileProperty = profileProperty; + } + + public String summary() { + return path; + } + + } + + public class VariableForProfiling { + private VariableMode mode; + private String name; + private PropertyWithType property; + + public VariableForProfiling(VariableMode mode, String name, PropertyWithType property) { + super(); + this.mode = mode; + this.name = name; + this.property = property; + } + public VariableMode getMode() { + return mode; + } + public String getName() { + return name; + } + public PropertyWithType getProperty() { + return property; + } + public String summary() { + return name+": "+property.summary(); + } + } + + public class VariablesForProfiling { + private List list = new ArrayList(); + private boolean optional; + private boolean repeating; + + public VariablesForProfiling(boolean optional, boolean repeating) { + this.optional = optional; + this.repeating = repeating; + } + + public void add(VariableMode mode, String name, String path, Property property, TypeDetails types) { + add(mode, name, new PropertyWithType(path, property, null, types)); + } + + public void add(VariableMode mode, String name, String path, Property baseProperty, Property profileProperty, TypeDetails types) { + add(mode, name, new PropertyWithType(path, baseProperty, profileProperty, types)); + } + + public void add(VariableMode mode, String name, PropertyWithType property) { + VariableForProfiling vv = null; + for (VariableForProfiling v : list) + if ((v.mode == mode) && v.getName().equals(name)) + vv = v; + if (vv != null) + list.remove(vv); + list.add(new VariableForProfiling(mode, name, property)); + } + + public VariablesForProfiling copy(boolean optional, boolean repeating) { + VariablesForProfiling result = new VariablesForProfiling(optional, repeating); + result.list.addAll(list); + return result; + } + + public VariablesForProfiling copy() { + VariablesForProfiling result = new VariablesForProfiling(optional, repeating); + result.list.addAll(list); + return result; + } + + public VariableForProfiling get(VariableMode mode, String name) { + if (mode == null) { + for (VariableForProfiling v : list) + if ((v.mode == VariableMode.OUTPUT) && v.getName().equals(name)) + return v; + for (VariableForProfiling v : list) + if ((v.mode == VariableMode.INPUT) && v.getName().equals(name)) + return v; + } + for (VariableForProfiling v : list) + if ((v.mode == mode) && v.getName().equals(name)) + return v; + return null; + } + + public String summary() { + CommaSeparatedStringBuilder s = new CommaSeparatedStringBuilder(); + CommaSeparatedStringBuilder t = new CommaSeparatedStringBuilder(); + for (VariableForProfiling v : list) + if (v.mode == VariableMode.INPUT) + s.append(v.summary()); + else + t.append(v.summary()); + return "source variables ["+s.toString()+"], target variables ["+t.toString()+"]"; + } + } + + public class StructureMapAnalysis { + private List profiles = new ArrayList(); + private XhtmlNode summary; + public List getProfiles() { + return profiles; + } + public XhtmlNode getSummary() { + return summary; + } + + } + + /** + * Given a structure map, return a set of analyses on it. + * + * Returned: + * - a list or profiles for what it will create. First profile is the target + * - a table with a summary (in xhtml) for easy human undertanding of the mapping + * + * + * @param appInfo + * @param map + * @return + * @throws Exception + */ + public StructureMapAnalysis analyse(Object appInfo, StructureMap map) throws Exception { + ids.clear(); + StructureMapAnalysis result = new StructureMapAnalysis(); + TransformContext context = new TransformContext(appInfo); + VariablesForProfiling vars = new VariablesForProfiling(false, false); + StructureMapGroupComponent start = map.getGroup().get(0); + for (StructureMapGroupInputComponent t : start.getInput()) { + PropertyWithType ti = resolveType(map, t.getType(), t.getMode()); + if (t.getMode() == StructureMapInputMode.SOURCE) + vars.add(VariableMode.INPUT, t.getName(), ti); + else + vars.add(VariableMode.OUTPUT, t.getName(), createProfile(map, result.profiles, ti, start.getName(), start)); + } + + result.summary = new XhtmlNode(NodeType.Element, "table").setAttribute("class", "grid"); + XhtmlNode tr = result.summary.addTag("tr"); + tr.addTag("td").addTag("b").addText("Source"); + tr.addTag("td").addTag("b").addText("Target"); + + log("Start Profiling Transform "+map.getUrl()); + analyseGroup("", context, map, vars, start, result); + ProfileUtilities pu = new ProfileUtilities(worker, null, pkp); + for (StructureDefinition sd : result.getProfiles()) + pu.cleanUpDifferential(sd); + return result; + } + + + private void analyseGroup(String indent, TransformContext context, StructureMap map, VariablesForProfiling vars, StructureMapGroupComponent group, StructureMapAnalysis result) throws Exception { + log(indent+"Analyse Group : "+group.getName()); + // todo: extends + // todo: check inputs + XhtmlNode tr = result.summary.addTag("tr").setAttribute("class", "diff-title"); + XhtmlNode xs = tr.addTag("td"); + XhtmlNode xt = tr.addTag("td"); + for (StructureMapGroupInputComponent inp : group.getInput()) { + if (inp.getMode() == StructureMapInputMode.SOURCE) + noteInput(vars, inp, VariableMode.INPUT, xs); + if (inp.getMode() == StructureMapInputMode.TARGET) + noteInput(vars, inp, VariableMode.OUTPUT, xt); + } + for (StructureMapGroupRuleComponent r : group.getRule()) { + analyseRule(indent+" ", context, map, vars, group, r, result); + } + } + + + private void noteInput(VariablesForProfiling vars, StructureMapGroupInputComponent inp, VariableMode mode, XhtmlNode xs) { + VariableForProfiling v = vars.get(mode, inp.getName()); + if (v != null) + xs.addText("Input: "+v.property.getPath()); + } + + private void analyseRule(String indent, TransformContext context, StructureMap map, VariablesForProfiling vars, StructureMapGroupComponent group, StructureMapGroupRuleComponent rule, StructureMapAnalysis result) throws Exception { + log(indent+"Analyse rule : "+rule.getName()); + XhtmlNode tr = result.summary.addTag("tr"); + XhtmlNode xs = tr.addTag("td"); + XhtmlNode xt = tr.addTag("td"); + + VariablesForProfiling srcVars = vars.copy(); + if (rule.getSource().size() != 1) + throw new Exception("Rule \""+rule.getName()+"\": not handled yet"); + VariablesForProfiling source = analyseSource(rule.getName(), context, srcVars, rule.getSourceFirstRep(), xs); + + TargetWriter tw = new TargetWriter(); + for (StructureMapGroupRuleTargetComponent t : rule.getTarget()) { + analyseTarget(rule.getName(), context, source, map, t, rule.getSourceFirstRep().getVariable(), tw, result.profiles, rule.getName()); + } + tw.commit(xt); + + for (StructureMapGroupRuleComponent childrule : rule.getRule()) { + analyseRule(indent+" ", context, map, source, group, childrule, result); + } +// for (StructureMapGroupRuleDependentComponent dependent : rule.getDependent()) { +// executeDependency(indent+" ", context, map, v, group, dependent); // do we need group here? +// } + } + + public class StringPair { + private String var; + private String desc; + public StringPair(String var, String desc) { + super(); + this.var = var; + this.desc = desc; + } + public String getVar() { + return var; + } + public String getDesc() { + return desc; + } + } + public class TargetWriter { + private Map newResources = new HashMap(); + private List assignments = new ArrayList(); + private List keyProps = new ArrayList(); + private CommaSeparatedStringBuilder txt = new CommaSeparatedStringBuilder(); + + public void newResource(String var, String name) { + newResources.put(var, name); + txt.append("new "+name); + } + + public void valueAssignment(String context, String desc) { + assignments.add(new StringPair(context, desc)); + txt.append(desc); + } + + public void keyAssignment(String context, String desc) { + keyProps.add(new StringPair(context, desc)); + txt.append(desc); + } + public void commit(XhtmlNode xt) { + if (newResources.size() == 1 && assignments.size() == 1 && newResources.containsKey(assignments.get(0).getVar()) && keyProps.size() == 1 && newResources.containsKey(keyProps.get(0).getVar()) ) { + xt.addText("new "+assignments.get(0).desc+" ("+keyProps.get(0).desc.substring(keyProps.get(0).desc.indexOf(".")+1)+")"); + } else if (newResources.size() == 1 && assignments.size() == 1 && newResources.containsKey(assignments.get(0).getVar()) && keyProps.size() == 0) { + xt.addText("new "+assignments.get(0).desc); + } else { + xt.addText(txt.toString()); + } + } + } + + private VariablesForProfiling analyseSource(String ruleId, TransformContext context, VariablesForProfiling vars, StructureMapGroupRuleSourceComponent src, XhtmlNode td) throws Exception { + VariableForProfiling var = vars.get(VariableMode.INPUT, src.getContext()); + if (var == null) + throw new FHIRException("Rule \""+ruleId+"\": Unknown input variable "+src.getContext()); + PropertyWithType prop = var.getProperty(); + + boolean optional = false; + boolean repeating = false; + + if (src.hasCondition()) { + optional = true; + } + + if (src.hasElement()) { + Property element = prop.getBaseProperty().getChild(prop.types.getType(), src.getElement()); + if (element == null) + throw new Exception("Rule \""+ruleId+"\": Unknown element name "+src.getElement()); + if (element.getDefinition().getMin() == 0) + optional = true; + if (element.getDefinition().getMax().equals("*")) + repeating = true; + VariablesForProfiling result = vars.copy(optional, repeating); + TypeDetails type = new TypeDetails(CollectionStatus.SINGLETON); + for (TypeRefComponent tr : element.getDefinition().getType()) { + if (!tr.hasCode()) + throw new Error("Rule \""+ruleId+"\": Element has no type"); + ProfiledType pt = new ProfiledType(tr.getCode()); + if (tr.hasProfile()) + pt.addProfile(tr.getProfile()); + if (element.getDefinition().hasBinding()) + pt.addBinding(element.getDefinition().getBinding()); + type.addType(pt); + } + td.addText(prop.getPath()+"."+src.getElement()); + if (src.hasVariable()) + result.add(VariableMode.INPUT, src.getVariable(), new PropertyWithType(prop.getPath()+"."+src.getElement(), element, null, type)); + return result; + } else { + td.addText(prop.getPath()); // ditto! + return vars.copy(optional, repeating); + } + } + + + private void analyseTarget(String ruleId, TransformContext context, VariablesForProfiling vars, StructureMap map, StructureMapGroupRuleTargetComponent tgt, String tv, TargetWriter tw, List profiles, String sliceName) throws Exception { + VariableForProfiling var = null; + if (tgt.hasContext()) { + var = vars.get(VariableMode.OUTPUT, tgt.getContext()); + if (var == null) + throw new Exception("Rule \""+ruleId+"\": target context not known: "+tgt.getContext()); + if (!tgt.hasElement()) + throw new Exception("Rule \""+ruleId+"\": Not supported yet"); + } + + + TypeDetails type = null; + if (tgt.hasTransform()) { + type = analyseTransform(context, map, tgt, var, vars); + // profiling: dest.setProperty(tgt.getElement().hashCode(), tgt.getElement(), v); + } else { + Property vp = var.property.baseProperty.getChild(tgt.getElement(), tgt.getElement()); + if (vp == null) + throw new Exception("Unknown Property "+tgt.getElement()+" on "+var.property.path); + + type = new TypeDetails(CollectionStatus.SINGLETON, vp.getType(tgt.getElement())); + } + + if (tgt.getTransform() == StructureMapTransform.CREATE) { + String s = getParamString(vars, tgt.getParameter().get(0)); + if (worker.getResourceNames().contains(s)) + tw.newResource(tgt.getVariable(), s); + } else { + boolean mapsSrc = false; + for (StructureMapGroupRuleTargetParameterComponent p : tgt.getParameter()) { + Type pr = p.getValue(); + if (pr instanceof IdType && ((IdType) pr).asStringValue().equals(tv)) + mapsSrc = true; + } + if (mapsSrc) { + if (var == null) + throw new Error("Rule \""+ruleId+"\": Attempt to assign with no context"); + tw.valueAssignment(tgt.getContext(), var.property.getPath()+"."+tgt.getElement()+getTransformSuffix(tgt.getTransform())); + } else if (tgt.hasContext()) { + if (isSignificantElement(var.property, tgt.getElement())) { + String td = describeTransform(tgt); + if (td != null) + tw.keyAssignment(tgt.getContext(), var.property.getPath()+"."+tgt.getElement()+" = "+td); + } + } + } + Type fixed = generateFixedValue(tgt); + + PropertyWithType prop = updateProfile(var, tgt.getElement(), type, map, profiles, sliceName, fixed, tgt); + if (tgt.hasVariable()) + if (tgt.hasElement()) + vars.add(VariableMode.OUTPUT, tgt.getVariable(), prop); + else + vars.add(VariableMode.OUTPUT, tgt.getVariable(), prop); + } + + private Type generateFixedValue(StructureMapGroupRuleTargetComponent tgt) { + if (!allParametersFixed(tgt)) + return null; + if (!tgt.hasTransform()) + return null; + switch (tgt.getTransform()) { + case COPY: return tgt.getParameter().get(0).getValue(); + case TRUNCATE: return null; + //case ESCAPE: + //case CAST: + //case APPEND: + case TRANSLATE: return null; + //case DATEOP, + //case UUID, + //case POINTER, + //case EVALUATE, + case CC: + CodeableConcept cc = new CodeableConcept(); + cc.addCoding(buildCoding(tgt.getParameter().get(0).getValue(), tgt.getParameter().get(1).getValue())); + return cc; + case C: + return buildCoding(tgt.getParameter().get(0).getValue(), tgt.getParameter().get(1).getValue()); + case QTY: return null; + //case ID, + //case CP, + default: + return null; + } + } + + @SuppressWarnings("rawtypes") + private Coding buildCoding(Type value1, Type value2) { + return new Coding().setSystem(((PrimitiveType) value1).asStringValue()).setCode(((PrimitiveType) value2).asStringValue()) ; + } + + private boolean allParametersFixed(StructureMapGroupRuleTargetComponent tgt) { + for (StructureMapGroupRuleTargetParameterComponent p : tgt.getParameter()) { + Type pr = p.getValue(); + if (pr instanceof IdType) + return false; + } + return true; + } + + private String describeTransform(StructureMapGroupRuleTargetComponent tgt) throws FHIRException { + switch (tgt.getTransform()) { + case COPY: return null; + case TRUNCATE: return null; + //case ESCAPE: + //case CAST: + //case APPEND: + case TRANSLATE: return null; + //case DATEOP, + //case UUID, + //case POINTER, + //case EVALUATE, + case CC: return describeTransformCCorC(tgt); + case C: return describeTransformCCorC(tgt); + case QTY: return null; + //case ID, + //case CP, + default: + return null; + } + } + + @SuppressWarnings("rawtypes") + private String describeTransformCCorC(StructureMapGroupRuleTargetComponent tgt) throws FHIRException { + if (tgt.getParameter().size() < 2) + return null; + Type p1 = tgt.getParameter().get(0).getValue(); + Type p2 = tgt.getParameter().get(1).getValue(); + if (p1 instanceof IdType || p2 instanceof IdType) + return null; + if (!(p1 instanceof PrimitiveType) || !(p2 instanceof PrimitiveType)) + return null; + String uri = ((PrimitiveType) p1).asStringValue(); + String code = ((PrimitiveType) p2).asStringValue(); + if (Utilities.noString(uri)) + throw new FHIRException("Describe Transform, but the uri is blank"); + if (Utilities.noString(code)) + throw new FHIRException("Describe Transform, but the code is blank"); + Coding c = buildCoding(uri, code); + return NarrativeGenerator.describeSystem(c.getSystem())+"#"+c.getCode()+(c.hasDisplay() ? "("+c.getDisplay()+")" : ""); + } + + + private boolean isSignificantElement(PropertyWithType property, String element) { + if ("Observation".equals(property.getPath())) + return "code".equals(element); + else if ("Bundle".equals(property.getPath())) + return "type".equals(element); + else + return false; + } + + private String getTransformSuffix(StructureMapTransform transform) { + switch (transform) { + case COPY: return ""; + case TRUNCATE: return " (truncated)"; + //case ESCAPE: + //case CAST: + //case APPEND: + case TRANSLATE: return " (translated)"; + //case DATEOP, + //case UUID, + //case POINTER, + //case EVALUATE, + case CC: return " (--> CodeableConcept)"; + case C: return " (--> Coding)"; + case QTY: return " (--> Quantity)"; + //case ID, + //case CP, + default: + return " {??)"; + } + } + + private PropertyWithType updateProfile(VariableForProfiling var, String element, TypeDetails type, StructureMap map, List profiles, String sliceName, Type fixed, StructureMapGroupRuleTargetComponent tgt) throws FHIRException { + if (var == null) { + assert (Utilities.noString(element)); + // 1. start the new structure definition + StructureDefinition sdn = worker.fetchResource(StructureDefinition.class, type.getType()); + if (sdn == null) + throw new FHIRException("Unable to find definition for "+type.getType()); + ElementDefinition edn = sdn.getSnapshot().getElementFirstRep(); + PropertyWithType pn = createProfile(map, profiles, new PropertyWithType(sdn.getId(), new Property(worker, edn, sdn), null, type), sliceName, tgt); + +// // 2. hook it into the base bundle +// if (type.getType().startsWith("http://hl7.org/fhir/StructureDefinition/") && worker.getResourceNames().contains(type.getType().substring(40))) { +// StructureDefinition sd = var.getProperty().profileProperty.getStructure(); +// ElementDefinition ed = sd.getDifferential().addElement(); +// ed.setPath("Bundle.entry"); +// ed.setName(sliceName); +// ed.setMax("1"); // well, it is for now... +// ed = sd.getDifferential().addElement(); +// ed.setPath("Bundle.entry.fullUrl"); +// ed.setMin(1); +// ed = sd.getDifferential().addElement(); +// ed.setPath("Bundle.entry.resource"); +// ed.setMin(1); +// ed.addType().setCode(pn.getProfileProperty().getStructure().getType()).setProfile(pn.getProfileProperty().getStructure().getUrl()); +// } + return pn; + } else { + assert (!Utilities.noString(element)); + Property pvb = var.getProperty().getBaseProperty(); + Property pvd = var.getProperty().getProfileProperty(); + Property pc = pvb.getChild(element, var.property.types); + if (pc == null) + throw new DefinitionException("Unable to find a definition for "+pvb.getDefinition().getPath()+"."+element); + + // the profile structure definition (derived) + StructureDefinition sd = var.getProperty().profileProperty.getStructure(); + ElementDefinition ednew = sd.getDifferential().addElement(); + ednew.setPath(var.getProperty().profileProperty.getDefinition().getPath()+"."+pc.getName()); + ednew.setUserData("slice-name", sliceName); + ednew.setFixed(fixed); + for (ProfiledType pt : type.getProfiledTypes()) { + if (pt.hasBindings()) + ednew.setBinding(pt.getBindings().get(0)); + if (pt.getUri().startsWith("http://hl7.org/fhir/StructureDefinition/")) { + String t = pt.getUri().substring(40); + t = checkType(t, pc, pt.getProfiles()); + if (t != null) { + if (pt.hasProfiles()) { + for (String p : pt.getProfiles()) + if (t.equals("Reference")) + ednew.addType().setCode(t).setTargetProfile(p); + else + ednew.addType().setCode(t).setProfile(p); + } else + ednew.addType().setCode(t); + } + } + } + + return new PropertyWithType(var.property.path+"."+element, pc, new Property(worker, ednew, sd), type); + } + } + + + + private String checkType(String t, Property pvb, List profiles) throws FHIRException { + if (pvb.getDefinition().getType().size() == 1 && isCompatibleType(t, pvb.getDefinition().getType().get(0).getCode()) && profilesMatch(profiles, pvb.getDefinition().getType().get(0).getProfile())) + return null; + for (TypeRefComponent tr : pvb.getDefinition().getType()) { + if (isCompatibleType(t, tr.getCode())) + return tr.getCode(); // note what is returned - the base type, not the inferred mapping type + } + throw new FHIRException("The type "+t+" is not compatible with the allowed types for "+pvb.getDefinition().getPath()); + } + + private boolean profilesMatch(List profiles, String profile) { + return profiles == null || profiles.size() == 0 || (profiles.size() == 1 && profiles.get(0).equals(profile)); + } + + private boolean isCompatibleType(String t, String code) { + if (t.equals(code)) + return true; + if (t.equals("string")) { + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+code); + if (sd != null && sd.getBaseDefinition().equals("http://hl7.org/fhir/StructureDefinition/string")) + return true; + } + return false; + } + + private TypeDetails analyseTransform(TransformContext context, StructureMap map, StructureMapGroupRuleTargetComponent tgt, VariableForProfiling var, VariablesForProfiling vars) throws FHIRException { + switch (tgt.getTransform()) { + case CREATE : + String p = getParamString(vars, tgt.getParameter().get(0)); + return new TypeDetails(CollectionStatus.SINGLETON, p); + case COPY : + return getParam(vars, tgt.getParameter().get(0)); + case EVALUATE : + ExpressionNode expr = (ExpressionNode) tgt.getUserData(MAP_EXPRESSION); + if (expr == null) { + expr = fpe.parse(getParamString(vars, tgt.getParameter().get(tgt.getParameter().size()-1))); + tgt.setUserData(MAP_WHERE_EXPRESSION, expr); + } + return fpe.check(vars, null, expr); + +////case TRUNCATE : +//// String src = getParamString(vars, tgt.getParameter().get(0)); +//// String len = getParamString(vars, tgt.getParameter().get(1)); +//// if (Utilities.isInteger(len)) { +//// int l = Integer.parseInt(len); +//// if (src.length() > l) +//// src = src.substring(0, l); +//// } +//// return new StringType(src); +////case ESCAPE : +//// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); +////case CAST : +//// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); +////case APPEND : +//// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); + case TRANSLATE : + return new TypeDetails(CollectionStatus.SINGLETON, "CodeableConcept"); + case CC: + ProfiledType res = new ProfiledType("CodeableConcept"); + if (tgt.getParameter().size() >= 2 && isParamId(vars, tgt.getParameter().get(1))) { + TypeDetails td = vars.get(null, getParamId(vars, tgt.getParameter().get(1))).property.types; + if (td != null && td.hasBinding()) + // todo: do we need to check that there's no implicit translation her? I don't think we do... + res.addBinding(td.getBinding()); + } + return new TypeDetails(CollectionStatus.SINGLETON, res); + case C: + return new TypeDetails(CollectionStatus.SINGLETON, "Coding"); + case QTY: + return new TypeDetails(CollectionStatus.SINGLETON, "Quantity"); + case REFERENCE : + VariableForProfiling vrs = vars.get(VariableMode.OUTPUT, getParamId(vars, tgt.getParameterFirstRep())); + if (vrs == null) + throw new FHIRException("Unable to resolve variable \""+getParamId(vars, tgt.getParameterFirstRep())+"\""); + String profile = vrs.property.getProfileProperty().getStructure().getUrl(); + TypeDetails td = new TypeDetails(CollectionStatus.SINGLETON); + td.addType("Reference", profile); + return td; +////case DATEOP : +//// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); +////case UUID : +//// return new IdType(UUID.randomUUID().toString()); +////case POINTER : +//// Base b = getParam(vars, tgt.getParameter().get(0)); +//// if (b instanceof Resource) +//// return new UriType("urn:uuid:"+((Resource) b).getId()); +//// else +//// throw new FHIRException("Transform engine cannot point at an element of type "+b.fhirType()); + default: + throw new Error("Transform Unknown or not handled yet: "+tgt.getTransform().toCode()); + } + } + private String getParamString(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) { + Type p = parameter.getValue(); + if (p == null || p instanceof IdType) + return null; + if (!p.hasPrimitiveValue()) + return null; + return p.primitiveValue(); + } + + private String getParamId(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) { + Type p = parameter.getValue(); + if (p == null || !(p instanceof IdType)) + return null; + return p.primitiveValue(); + } + + private boolean isParamId(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) { + Type p = parameter.getValue(); + if (p == null || !(p instanceof IdType)) + return false; + return vars.get(null, p.primitiveValue()) != null; + } + + private TypeDetails getParam(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) throws DefinitionException { + Type p = parameter.getValue(); + if (!(p instanceof IdType)) + return new TypeDetails(CollectionStatus.SINGLETON, "http://hl7.org/fhir/StructureDefinition/"+p.fhirType()); + else { + String n = ((IdType) p).asStringValue(); + VariableForProfiling b = vars.get(VariableMode.INPUT, n); + if (b == null) + b = vars.get(VariableMode.OUTPUT, n); + if (b == null) + throw new DefinitionException("Variable "+n+" not found ("+vars.summary()+")"); + return b.getProperty().getTypes(); + } + } + + private PropertyWithType createProfile(StructureMap map, List profiles, PropertyWithType prop, String sliceName, Base ctxt) throws DefinitionException { + if (prop.getBaseProperty().getDefinition().getPath().contains(".")) + throw new DefinitionException("Unable to process entry point"); + + String type = prop.getBaseProperty().getDefinition().getPath(); + String suffix = ""; + if (ids.containsKey(type)) { + int id = ids.get(type); + id++; + ids.put(type, id); + suffix = "-"+Integer.toString(id); + } else + ids.put(type, 0); + + StructureDefinition profile = new StructureDefinition(); + profiles.add(profile); + profile.setDerivation(TypeDerivationRule.CONSTRAINT); + profile.setType(type); + profile.setBaseDefinition(prop.getBaseProperty().getStructure().getUrl()); + profile.setName("Profile for "+profile.getType()+" for "+sliceName); + profile.setUrl(map.getUrl().replace("StructureMap", "StructureDefinition")+"-"+profile.getType()+suffix); + ctxt.setUserData("profile", profile.getUrl()); // then we can easily assign this profile url for validation later when we actually transform + profile.setId(map.getId()+"-"+profile.getType()+suffix); + profile.setStatus(map.getStatus()); + profile.setExperimental(map.getExperimental()); + profile.setDescription("Generated automatically from the mapping by the Java Reference Implementation"); + for (ContactDetail c : map.getContact()) { + ContactDetail p = profile.addContact(); + p.setName(c.getName()); + for (ContactPoint cc : c.getTelecom()) + p.addTelecom(cc); + } + profile.setDate(map.getDate()); + profile.setCopyright(map.getCopyright()); + profile.setFhirVersion(Constants.VERSION); + profile.setKind(prop.getBaseProperty().getStructure().getKind()); + profile.setAbstract(false); + ElementDefinition ed = profile.getDifferential().addElement(); + ed.setPath(profile.getType()); + prop.profileProperty = new Property(worker, ed, profile); + return prop; + } + + private PropertyWithType resolveType(StructureMap map, String type, StructureMapInputMode mode) throws Exception { + for (StructureMapStructureComponent imp : map.getStructure()) { + if ((imp.getMode() == StructureMapModelMode.SOURCE && mode == StructureMapInputMode.SOURCE) || + (imp.getMode() == StructureMapModelMode.TARGET && mode == StructureMapInputMode.TARGET)) { + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, imp.getUrl()); + if (sd == null) + throw new Exception("Import "+imp.getUrl()+" cannot be resolved"); + if (sd.getId().equals(type)) { + return new PropertyWithType(sd.getType(), new Property(worker, sd.getSnapshot().getElement().get(0), sd), null, new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl())); + } + } + } + throw new Exception("Unable to find structure definition for "+type+" in imports"); + } + + + public StructureMap generateMapFromMappings(StructureDefinition sd) throws IOException, FHIRException { + String id = getLogicalMappingId(sd); + if (id == null) + return null; + String prefix = ToolingExtensions.readStringExtension(sd, ToolingExtensions.EXT_MAPPING_PREFIX); + String suffix = ToolingExtensions.readStringExtension(sd, ToolingExtensions.EXT_MAPPING_SUFFIX); + if (prefix == null || suffix == null) + return null; + // we build this by text. Any element that has a mapping, we put it's mappings inside it.... + StringBuilder b = new StringBuilder(); + b.append(prefix); + + ElementDefinition root = sd.getSnapshot().getElementFirstRep(); + String m = getMapping(root, id); + if (m != null) + b.append(m+"\r\n"); + addChildMappings(b, id, "", sd, root, false); + b.append("\r\n"); + b.append(suffix); + b.append("\r\n"); + TextFile.stringToFile(b.toString(), "c:\\temp\\test.map"); + StructureMap map = parse(b.toString()); + map.setId(tail(map.getUrl())); + if (!map.hasStatus()) + map.setStatus(PublicationStatus.DRAFT); + map.getText().setStatus(NarrativeStatus.GENERATED); + map.getText().setDiv(new XhtmlNode(NodeType.Element, "div")); + map.getText().getDiv().addTag("pre").addText(render(map)); + return map; + } + + + private String tail(String url) { + return url.substring(url.lastIndexOf("/")+1); + } + + + private void addChildMappings(StringBuilder b, String id, String indent, StructureDefinition sd, ElementDefinition ed, boolean inner) throws DefinitionException { + boolean first = true; + List children = ProfileUtilities.getChildMap(sd, ed); + for (ElementDefinition child : children) { + if (first && inner) { + b.append(" then {\r\n"); + first = false; + } + String map = getMapping(child, id); + if (map != null) { + b.append(indent+" "+child.getPath()+": "+map); + addChildMappings(b, id, indent+" ", sd, child, true); + b.append("\r\n"); + } + } + if (!first && inner) + b.append(indent+"}"); + + } + + + private String getMapping(ElementDefinition ed, String id) { + for (ElementDefinitionMappingComponent map : ed.getMapping()) + if (id.equals(map.getIdentity())) + return map.getMap(); + return null; + } + + + private String getLogicalMappingId(StructureDefinition sd) { + String id = null; + for (StructureDefinitionMappingComponent map : sd.getMapping()) { + if ("http://hl7.org/fhir/logical".equals(map.getUri())) + return map.getIdentity(); + } + return null; + } + +} diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/formats/JsonTrackingParser.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/formats/JsonTrackingParser.java index 76147fd9fab..dd7eaaed48e 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/formats/JsonTrackingParser.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/formats/JsonTrackingParser.java @@ -1,498 +1,505 @@ -package org.hl7.fhir.dstu3.utils.formats; - -import java.math.BigDecimal; -import java.util.Map; -import java.util.Stack; - -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.utilities.Utilities; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; - - -/** - * This is created to get a json parser that can track line numbers... grr... - * - * @author Grahame Grieve - * - */ -public class JsonTrackingParser { - - public enum TokenType { - Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean; - } - - public class LocationData { - private int line; - private int col; - - protected LocationData(int line, int col) { - super(); - this.line = line; - this.col = col; - } - - public int getLine() { - return line; - } - - public int getCol() { - return col; - } - - public void newLine() { - line++; - col = 1; - } - - public LocationData copy() { - return new LocationData(line, col); - } - } - - private class State { - private String name; - private boolean isProp; - protected State(String name, boolean isProp) { - super(); - this.name = name; - this.isProp = isProp; - } - public String getName() { - return name; - } - public boolean isProp() { - return isProp; - } - } - - private class Lexer { - private String source; - private int cursor; - private String peek; - private String value; - private TokenType type; - private Stack states = new Stack(); - private LocationData lastLocationBWS; - private LocationData lastLocationAWS; - private LocationData location; - private StringBuilder b = new StringBuilder(); - - public Lexer(String source) throws FHIRException { - this.source = source; - cursor = -1; - location = new LocationData(1, 1); - start(); - } - - private boolean more() { - return peek != null || cursor < source.length(); - } - - private String getNext(int length) throws FHIRException { - String result = ""; - if (peek != null) { - if (peek.length() > length) { - result = peek.substring(0, length); - peek = peek.substring(length); - } else { - result = peek; - peek = null; - } - } - if (result.length() < length) { - int len = length - result.length(); - if (cursor > source.length() - len) - throw error("Attempt to read past end of source"); - result = result + source.substring(cursor+1, cursor+len+1); - cursor = cursor + len; - } - for (char ch : result.toCharArray()) - if (ch == '\n') - location.newLine(); - else - location.col++; - return result; - } - - private char getNextChar() throws FHIRException { - if (peek != null) { - char ch = peek.charAt(0); - peek = peek.length() == 1 ? null : peek.substring(1); - return ch; - } else { - cursor++; - if (cursor >= source.length()) - return (char) 0; - char ch = source.charAt(cursor); - if (ch == '\n') { - location.newLine(); - } else { - location.col++; - } - return ch; - } - } - - private void push(char ch){ - peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek; - } - - private void parseWord(String word, char ch, TokenType type) throws FHIRException { - this.type = type; - value = ""+ch+getNext(word.length()-1); - if (!value.equals(word)) - throw error("Syntax error in json reading special word "+word); - } - - private FHIRException error(String msg) { - return new FHIRException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.line)+" (path=["+path()+"])"); - } - - private String path() { - if (states.empty()) - return value; - else { - String result = ""; - for (State s : states) - result = result + '/'+ s.getName(); - result = result + value; - return result; - } - } - - public void start() throws FHIRException { -// char ch = getNextChar(); -// if (ch = '\.uEF') -// begin -// // skip BOM -// getNextChar(); -// getNextChar(); -// end -// else -// push(ch); - next(); - } - - public TokenType getType() { - return type; - } - - public String getValue() { - return value; - } - - - public LocationData getLastLocationBWS() { - return lastLocationBWS; - } - - public LocationData getLastLocationAWS() { - return lastLocationAWS; - } - - public void next() throws FHIRException { - lastLocationBWS = location.copy(); - char ch; - do { - ch = getNextChar(); - } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t')); - lastLocationAWS = location.copy(); - - if (!more()) { - type = TokenType.Eof; - } else { - switch (ch) { - case '{' : - type = TokenType.Open; - break; - case '}' : - type = TokenType.Close; - break; - case '"' : - type = TokenType.String; - b.setLength(0); - do { - ch = getNextChar(); - if (ch == '\\') { - ch = getNextChar(); - switch (ch) { - case '"': b.append('"'); break; - case '\\': b.append('\\'); break; - case '/': b.append('/'); break; - case 'n': b.append('\n'); break; - case 'r': b.append('\r'); break; - case 't': b.append('\t'); break; - case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break; - default : - throw error("unknown escape sequence: \\"+ch); - } - ch = ' '; - } else if (ch != '"') - b.append(ch); - } while (more() && (ch != '"')); - if (!more()) - throw error("premature termination of json stream during a string"); - value = b.toString(); - break; - case ':' : - type = TokenType.Colon; - break; - case ',' : - type = TokenType.Comma; - break; - case '[' : - type = TokenType.OpenArray; - break; - case ']' : - type = TokenType.CloseArray; - break; - case 't' : - parseWord("true", ch, TokenType.Boolean); - break; - case 'f' : - parseWord("false", ch, TokenType.Boolean); - break; - case 'n' : - parseWord("null", ch, TokenType.Null); - break; - default: - if ((ch >= '0' && ch <= '9') || ch == '-') { - type = TokenType.Number; - b.setLength(0); - while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.')) { - b.append(ch); - ch = getNextChar(); - } - value = b.toString(); - push(ch); - } else - throw error("Unexpected char '"+ch+"' in json stream"); - } - } - } - - public String consume(TokenType type) throws FHIRException { - if (this.type != type) - throw error("JSON syntax error - found "+type.toString()+" expecting "+type.toString()); - String result = value; - next(); - return result; - } - - } - - enum ItemType { - Object, String, Number, Boolean, Array, End, Eof, Null; - } - private Map map; - private Lexer lexer; - private ItemType itemType = ItemType.Object; - private String itemName; - private String itemValue; - - public static JsonObject parse(String source, Map map) throws FHIRException { - JsonTrackingParser self = new JsonTrackingParser(); - self.map = map; - return self.parse(source); - } - - private JsonObject parse(String source) throws FHIRException { - lexer = new Lexer(source); - JsonObject result = new JsonObject(); - LocationData loc = lexer.location.copy(); - if (lexer.getType() == TokenType.Open) { - lexer.next(); - lexer.states.push(new State("", false)); - } - else - throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString()); - - parseProperty(); - readObject(result, true); - map.put(result, loc); - return result; - } - - private void readObject(JsonObject obj, boolean root) throws FHIRException { - map.put(obj, lexer.location.copy()); - - while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) { - if (obj.has(itemName)) - throw lexer.error("Duplicated property name: "+itemName); - - switch (itemType) { - case Object: - JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName); - LocationData loc = lexer.location.copy(); - obj.add(itemName, child); - next(); - readObject(child, false); - map.put(obj, loc); - break; - case Boolean : - JsonPrimitive v = new JsonPrimitive(Boolean.valueOf(itemValue)); - obj.add(itemName, v); - map.put(v, lexer.location.copy()); - break; - case String: - v = new JsonPrimitive(itemValue); - obj.add(itemName, v); - map.put(v, lexer.location.copy()); - break; - case Number: - v = new JsonPrimitive(new BigDecimal(itemValue)); - obj.add(itemName, v); - map.put(v, lexer.location.copy()); - break; - case Null: - JsonNull n = new JsonNull(); - obj.add(itemName, n); - map.put(n, lexer.location.copy()); - break; - case Array: - JsonArray arr = new JsonArray(); // (obj.path+'.'+ItemName); - loc = lexer.location.copy(); - obj.add(itemName, arr); - next(); - readArray(arr, false); - map.put(arr, loc); - break; - case Eof : - throw lexer.error("Unexpected End of File"); - } - next(); - } - } - - private void readArray(JsonArray arr, boolean root) throws FHIRException { - while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) { - switch (itemType) { - case Object: - JsonObject obj = new JsonObject(); // (arr.path+'['+inttostr(i)+']'); - LocationData loc = lexer.location.copy(); - arr.add(obj); - next(); - readObject(obj, false); - map.put(obj, loc); - break; - case String: - JsonPrimitive v = new JsonPrimitive(itemValue); - arr.add(v); - map.put(v, lexer.location.copy()); - break; - case Number: - v = new JsonPrimitive(new BigDecimal(itemValue)); - arr.add(v); - map.put(v, lexer.location.copy()); - break; - case Null : - JsonNull n = new JsonNull(); - arr.add(n); - map.put(n, lexer.location.copy()); - break; - case Array: - JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']'); - loc = lexer.location.copy(); - arr.add(child); - next(); - readArray(child, false); - map.put(arr, loc); - break; - case Eof : - throw lexer.error("Unexpected End of File"); - } - next(); - } - } - - private void next() throws FHIRException { - switch (itemType) { - case Object : - lexer.consume(TokenType.Open); - lexer.states.push(new State(itemName, false)); - if (lexer.getType() == TokenType.Close) { - itemType = ItemType.End; - lexer.next(); - } else - parseProperty(); - break; - case Null: - case String: - case Number: - case End: - case Boolean : - if (itemType == ItemType.End) - lexer.states.pop(); - if (lexer.getType() == TokenType.Comma) { - lexer.next(); - parseProperty(); - } else if (lexer.getType() == TokenType.Close) { - itemType = ItemType.End; - lexer.next(); - } else if (lexer.getType() == TokenType.CloseArray) { - itemType = ItemType.End; - lexer.next(); - } else if (lexer.getType() == TokenType.Eof) { - itemType = ItemType.Eof; - } else - throw lexer.error("Unexpected JSON syntax"); - break; - case Array : - lexer.next(); - lexer.states.push(new State(itemName+"[]", true)); - parseProperty(); - break; - case Eof : - throw lexer.error("JSON Syntax Error - attempt to read past end of json stream"); - default: - throw lexer.error("not done yet (a): "+itemType.toString()); - } - } - - private void parseProperty() throws FHIRException { - if (!lexer.states.peek().isProp) { - itemName = lexer.consume(TokenType.String); - itemValue = null; - lexer.consume(TokenType.Colon); - } - switch (lexer.getType()) { - case Null : - itemType = ItemType.Null; - itemValue = lexer.value; - lexer.next(); - break; - case String : - itemType = ItemType.String; - itemValue = lexer.value; - lexer.next(); - break; - case Boolean : - itemType = ItemType.Boolean; - itemValue = lexer.value; - lexer.next(); - break; - case Number : - itemType = ItemType.Number; - itemValue = lexer.value; - lexer.next(); - break; - case Open : - itemType = ItemType.Object; - break; - case OpenArray : - itemType = ItemType.Array; - break; - case CloseArray : - itemType = ItemType.End; - break; - // case Close, , case Colon, case Comma, case OpenArray, ! - default: - throw lexer.error("not done yet (b): "+lexer.getType().toString()); - } - } -} +package org.hl7.fhir.dstu3.utils.formats; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.Stack; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.utilities.Utilities; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + + +/** + * This is created to get a json parser that can track line numbers... grr... + * + * @author Grahame Grieve + * + */ +public class JsonTrackingParser { + + public enum TokenType { + Open, Close, String, Number, Colon, Comma, OpenArray, CloseArray, Eof, Null, Boolean; + } + + public class LocationData { + private int line; + private int col; + + protected LocationData(int line, int col) { + super(); + this.line = line; + this.col = col; + } + + public int getLine() { + return line; + } + + public int getCol() { + return col; + } + + public void newLine() { + line++; + col = 1; + } + + public LocationData copy() { + return new LocationData(line, col); + } + } + + private class State { + private String name; + private boolean isProp; + protected State(String name, boolean isProp) { + super(); + this.name = name; + this.isProp = isProp; + } + public String getName() { + return name; + } + public boolean isProp() { + return isProp; + } + } + + private class Lexer { + private String source; + private int cursor; + private String peek; + private String value; + private TokenType type; + private Stack states = new Stack(); + private LocationData lastLocationBWS; + private LocationData lastLocationAWS; + private LocationData location; + private StringBuilder b = new StringBuilder(); + + public Lexer(String source) throws FHIRException { + this.source = source; + cursor = -1; + location = new LocationData(1, 1); + start(); + } + + private boolean more() { + return peek != null || cursor < source.length(); + } + + private String getNext(int length) throws FHIRException { + String result = ""; + if (peek != null) { + if (peek.length() > length) { + result = peek.substring(0, length); + peek = peek.substring(length); + } else { + result = peek; + peek = null; + } + } + if (result.length() < length) { + int len = length - result.length(); + if (cursor > source.length() - len) + throw error("Attempt to read past end of source"); + result = result + source.substring(cursor+1, cursor+len+1); + cursor = cursor + len; + } + for (char ch : result.toCharArray()) + if (ch == '\n') + location.newLine(); + else + location.col++; + return result; + } + + private char getNextChar() throws FHIRException { + if (peek != null) { + char ch = peek.charAt(0); + peek = peek.length() == 1 ? null : peek.substring(1); + return ch; + } else { + cursor++; + if (cursor >= source.length()) + return (char) 0; + char ch = source.charAt(cursor); + if (ch == '\n') { + location.newLine(); + } else { + location.col++; + } + return ch; + } + } + + private void push(char ch){ + peek = peek == null ? String.valueOf(ch) : String.valueOf(ch)+peek; + } + + private void parseWord(String word, char ch, TokenType type) throws FHIRException { + this.type = type; + value = ""+ch+getNext(word.length()-1); + if (!value.equals(word)) + throw error("Syntax error in json reading special word "+word); + } + + private FHIRException error(String msg) { + return new FHIRException("Error parsing JSON source: "+msg+" at Line "+Integer.toString(location.line)+" (path=["+path()+"])"); + } + + private String path() { + if (states.empty()) + return value; + else { + String result = ""; + for (State s : states) + result = result + '/'+ s.getName(); + result = result + value; + return result; + } + } + + public void start() throws FHIRException { +// char ch = getNextChar(); +// if (ch = '\.uEF') +// begin +// // skip BOM +// getNextChar(); +// getNextChar(); +// end +// else +// push(ch); + next(); + } + + public TokenType getType() { + return type; + } + + public String getValue() { + return value; + } + + + public LocationData getLastLocationBWS() { + return lastLocationBWS; + } + + public LocationData getLastLocationAWS() { + return lastLocationAWS; + } + + public void next() throws FHIRException { + lastLocationBWS = location.copy(); + char ch; + do { + ch = getNextChar(); + } while (more() && Utilities.charInSet(ch, ' ', '\r', '\n', '\t')); + lastLocationAWS = location.copy(); + + if (!more()) { + type = TokenType.Eof; + } else { + switch (ch) { + case '{' : + type = TokenType.Open; + break; + case '}' : + type = TokenType.Close; + break; + case '"' : + type = TokenType.String; + b.setLength(0); + do { + ch = getNextChar(); + if (ch == '\\') { + ch = getNextChar(); + switch (ch) { + case '"': b.append('"'); break; + case '\\': b.append('\\'); break; + case '/': b.append('/'); break; + case 'n': b.append('\n'); break; + case 'r': b.append('\r'); break; + case 't': b.append('\t'); break; + case 'u': b.append((char) Integer.parseInt(getNext(4), 16)); break; + default : + throw error("unknown escape sequence: \\"+ch); + } + ch = ' '; + } else if (ch != '"') + b.append(ch); + } while (more() && (ch != '"')); + if (!more()) + throw error("premature termination of json stream during a string"); + value = b.toString(); + break; + case ':' : + type = TokenType.Colon; + break; + case ',' : + type = TokenType.Comma; + break; + case '[' : + type = TokenType.OpenArray; + break; + case ']' : + type = TokenType.CloseArray; + break; + case 't' : + parseWord("true", ch, TokenType.Boolean); + break; + case 'f' : + parseWord("false", ch, TokenType.Boolean); + break; + case 'n' : + parseWord("null", ch, TokenType.Null); + break; + default: + if ((ch >= '0' && ch <= '9') || ch == '-') { + type = TokenType.Number; + b.setLength(0); + while (more() && ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.')) { + b.append(ch); + ch = getNextChar(); + } + value = b.toString(); + push(ch); + } else + throw error("Unexpected char '"+ch+"' in json stream"); + } + } + } + + public String consume(TokenType type) throws FHIRException { + if (this.type != type) + throw error("JSON syntax error - found "+type.toString()+" expecting "+type.toString()); + String result = value; + next(); + return result; + } + + } + + enum ItemType { + Object, String, Number, Boolean, Array, End, Eof, Null; + } + private Map map; + private Lexer lexer; + private ItemType itemType = ItemType.Object; + private String itemName; + private String itemValue; + + public static JsonObject parse(String source, Map map) throws FHIRException { + JsonTrackingParser self = new JsonTrackingParser(); + self.map = map; + return self.parse(source); + } + + private JsonObject parse(String source) throws FHIRException { + lexer = new Lexer(source); + JsonObject result = new JsonObject(); + LocationData loc = lexer.location.copy(); + if (lexer.getType() == TokenType.Open) { + lexer.next(); + lexer.states.push(new State("", false)); + } + else + throw lexer.error("Unexpected content at start of JSON: "+lexer.getType().toString()); + + parseProperty(); + readObject(result, true); + map.put(result, loc); + return result; + } + + private void readObject(JsonObject obj, boolean root) throws FHIRException { + map.put(obj, lexer.location.copy()); + + while (!(itemType == ItemType.End) || (root && (itemType == ItemType.Eof))) { + if (obj.has(itemName)) + throw lexer.error("Duplicated property name: "+itemName); + + switch (itemType) { + case Object: + JsonObject child = new JsonObject(); //(obj.path+'.'+ItemName); + LocationData loc = lexer.location.copy(); + obj.add(itemName, child); + next(); + readObject(child, false); + map.put(obj, loc); + break; + case Boolean : + JsonPrimitive v = new JsonPrimitive(Boolean.valueOf(itemValue)); + obj.add(itemName, v); + map.put(v, lexer.location.copy()); + break; + case String: + v = new JsonPrimitive(itemValue); + obj.add(itemName, v); + map.put(v, lexer.location.copy()); + break; + case Number: + v = new JsonPrimitive(new BigDecimal(itemValue)); + obj.add(itemName, v); + map.put(v, lexer.location.copy()); + break; + case Null: + JsonNull n = new JsonNull(); + obj.add(itemName, n); + map.put(n, lexer.location.copy()); + break; + case Array: + JsonArray arr = new JsonArray(); // (obj.path+'.'+ItemName); + loc = lexer.location.copy(); + obj.add(itemName, arr); + next(); + readArray(arr, false); + map.put(arr, loc); + break; + case Eof : + throw lexer.error("Unexpected End of File"); + case End: + // TODO: anything? + break; + } + next(); + } + } + + private void readArray(JsonArray arr, boolean root) throws FHIRException { + while (!((itemType == ItemType.End) || (root && (itemType == ItemType.Eof)))) { + switch (itemType) { + case Object: + JsonObject obj = new JsonObject(); // (arr.path+'['+inttostr(i)+']'); + LocationData loc = lexer.location.copy(); + arr.add(obj); + next(); + readObject(obj, false); + map.put(obj, loc); + break; + case String: + JsonPrimitive v = new JsonPrimitive(itemValue); + arr.add(v); + map.put(v, lexer.location.copy()); + break; + case Number: + v = new JsonPrimitive(new BigDecimal(itemValue)); + arr.add(v); + map.put(v, lexer.location.copy()); + break; + case Null : + JsonNull n = new JsonNull(); + arr.add(n); + map.put(n, lexer.location.copy()); + break; + case Array: + JsonArray child = new JsonArray(); // (arr.path+'['+inttostr(i)+']'); + loc = lexer.location.copy(); + arr.add(child); + next(); + readArray(child, false); + map.put(arr, loc); + break; + case Eof : + throw lexer.error("Unexpected End of File"); + case End: + case Boolean: + // TODO: anything? + break; + } + next(); + } + } + + private void next() throws FHIRException { + switch (itemType) { + case Object : + lexer.consume(TokenType.Open); + lexer.states.push(new State(itemName, false)); + if (lexer.getType() == TokenType.Close) { + itemType = ItemType.End; + lexer.next(); + } else + parseProperty(); + break; + case Null: + case String: + case Number: + case End: + case Boolean : + if (itemType == ItemType.End) + lexer.states.pop(); + if (lexer.getType() == TokenType.Comma) { + lexer.next(); + parseProperty(); + } else if (lexer.getType() == TokenType.Close) { + itemType = ItemType.End; + lexer.next(); + } else if (lexer.getType() == TokenType.CloseArray) { + itemType = ItemType.End; + lexer.next(); + } else if (lexer.getType() == TokenType.Eof) { + itemType = ItemType.Eof; + } else + throw lexer.error("Unexpected JSON syntax"); + break; + case Array : + lexer.next(); + lexer.states.push(new State(itemName+"[]", true)); + parseProperty(); + break; + case Eof : + throw lexer.error("JSON Syntax Error - attempt to read past end of json stream"); + default: + throw lexer.error("not done yet (a): "+itemType.toString()); + } + } + + private void parseProperty() throws FHIRException { + if (!lexer.states.peek().isProp) { + itemName = lexer.consume(TokenType.String); + itemValue = null; + lexer.consume(TokenType.Colon); + } + switch (lexer.getType()) { + case Null : + itemType = ItemType.Null; + itemValue = lexer.value; + lexer.next(); + break; + case String : + itemType = ItemType.String; + itemValue = lexer.value; + lexer.next(); + break; + case Boolean : + itemType = ItemType.Boolean; + itemValue = lexer.value; + lexer.next(); + break; + case Number : + itemType = ItemType.Number; + itemValue = lexer.value; + lexer.next(); + break; + case Open : + itemType = ItemType.Object; + break; + case OpenArray : + itemType = ItemType.Array; + break; + case CloseArray : + itemType = ItemType.End; + break; + // case Close, , case Colon, case Comma, case OpenArray, ! + default: + throw lexer.error("not done yet (b): "+lexer.getType().toString()); + } + } +} diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/FHIRPathEngine.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/FHIRPathEngine.java index 0b2d291b8fd..1ef92fc5404 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/FHIRPathEngine.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/FHIRPathEngine.java @@ -1,2670 +1,2671 @@ -package org.hl7.fhir.instance.utils; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Date; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.hl7.fhir.instance.model.Base; -import org.hl7.fhir.instance.model.BooleanType; -import org.hl7.fhir.instance.model.DateTimeType; -import org.hl7.fhir.instance.model.DateType; -import org.hl7.fhir.instance.model.DecimalType; -import org.hl7.fhir.instance.model.ElementDefinition; -import org.hl7.fhir.instance.model.ElementDefinition.TypeRefComponent; -import org.hl7.fhir.instance.model.ExpressionNode; -import org.hl7.fhir.instance.model.ExpressionNode.CollectionStatus; -import org.hl7.fhir.instance.model.ExpressionNode.Function; -import org.hl7.fhir.instance.model.ExpressionNode.Kind; -import org.hl7.fhir.instance.model.ExpressionNode.Operation; -import org.hl7.fhir.instance.model.ExpressionNode.SourceLocation; -import org.hl7.fhir.instance.model.ExpressionNode.TypeDetails; -import org.hl7.fhir.instance.model.IntegerType; -import org.hl7.fhir.instance.model.Resource; -import org.hl7.fhir.instance.model.StringType; -import org.hl7.fhir.instance.model.StructureDefinition; -import org.hl7.fhir.instance.model.TemporalPrecisionEnum; -import org.hl7.fhir.instance.model.TimeType; -import org.hl7.fhir.instance.model.Type; -import org.hl7.fhir.instance.utils.FHIRLexer.FHIRLexerException; -import org.hl7.fhir.instance.utils.FHIRPathEngine.IEvaluationContext.FunctionDetails; -import org.hl7.fhir.exceptions.DefinitionException; -import org.hl7.fhir.exceptions.PathEngineException; -import org.hl7.fhir.exceptions.UcumException; -import org.hl7.fhir.utilities.Utilities; -import org.hl7.fhir.utilities.ucum.Decimal; - - -/** - * - * @author Grahame Grieve - * - */ -public class FHIRPathEngine { - 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; - } - - } - - public Type resolveConstant(Object appContext, String name); - public String resolveConstantType(Object appContext, String name); - 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); - } - - - /** - * @param worker - used when validating paths (@check), and used doing value set membership when executing tests (once that's defined) - */ - public FHIRPathEngine(IWorkerContext worker) { - super(); - this.worker = worker; - primitiveTypes.add("string"); - primitiveTypes.add("code"); - primitiveTypes.add("integer"); - primitiveTypes.add("boolean"); - primitiveTypes.add("decimal"); - primitiveTypes.add("date"); - primitiveTypes.add("dateTime"); -// for (StructureDefinition sd : worker.allStructures()) { -// if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) -// allTypes.put(sd.getName(), sd); -// if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) { -// primitiveTypes.add(sd.getName()); -// } -// } - } - - - // --- 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 extensing the base model - - public IEvaluationContext getConstantResolver() { - return hostServices; - } - - - public void setConstantResolver(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 - * @ - */ - protected void getChildrenByName(Base item, String name, List result) { - List list = item.listChildrenByName(name); - 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 { - path = path.replace("$.", "$this."); - 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; - } - - /** - * 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 - * @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.contains(".")) - types = new TypeDetails(CollectionStatus.SINGLETON, context); - else { - StructureDefinition sd = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+context.substring(0, context.indexOf('.'))); - 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, 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, String resourceType, String context, String expr) throws FHIRLexerException, PathEngineException, DefinitionException { - return check(appContext, resourceType, context, parse(expr)); - } - - /** - * 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 PathEngineException - * @ - * @ - */ - public List evaluate(Base base, ExpressionNode ExpressionNode) throws PathEngineException { - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(null, null, base, 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 FHIRLexerException - * @throws PathEngineException - * @ - * @ - */ - public List evaluate(Base base, String path) throws FHIRLexerException, PathEngineException { - ExpressionNode exp = parse(path); - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(null, null, base, 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 PathEngineException - * @ - * @ - */ - public List evaluate(Object appContext, Resource resource, Base base, ExpressionNode ExpressionNode) throws PathEngineException { - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(appContext, resource, base, 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 PathEngineException - * @ - * @ - */ - public List evaluate(Object appContext, Base resource, Base base, ExpressionNode ExpressionNode) throws PathEngineException { - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(appContext, resource, base, 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 PathEngineException - * @throws FHIRLexerException - * @ - * @ - */ - public List evaluate(Object appContext, Resource resource, Base base, String path) throws PathEngineException, FHIRLexerException { - ExpressionNode exp = parse(path); - List list = new ArrayList(); - if (base != null) - list.add(base); - log = new StringBuilder(); - return execute(new ExecutionContext(appContext, resource, base, 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 FHIRLexerException - * @throws PathEngineException - * @ - * @ - */ - public boolean evaluateToBoolean(Resource resource, Base base, String path) throws PathEngineException, FHIRLexerException { - 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 - * @param path - the FHIR Path statement to use - * @return - * @throws PathEngineException - * @ - * @ - */ - public boolean evaluateToBoolean(Resource resource, Base base, ExpressionNode node) throws PathEngineException { - return convertToBoolean(evaluate(null, 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 - * @param path - the FHIR Path statement to use - * @return - * @throws PathEngineException - * @ - * @ - */ - public boolean evaluateToBoolean(Base resource, Base base, ExpressionNode node) throws PathEngineException { - 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 FHIRLexerException - * @throws PathEngineException - * @ - * @ - */ - public String evaluateToString(Base base, String path) throws FHIRLexerException, PathEngineException { - return convertToString(evaluate(base, path)); - } - - /** - * 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.getClass().getName(); - } - - /** - * 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; - else - log.append(","); - log.append(convertToString(b)); - } - } - } - - 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; - public ExecutionContext(Object appInfo, Base resource, Base context, Base thisItem) { - this.appInfo = appInfo; - this.resource = resource; - this.context = context; - this.thisItem = thisItem; - } - 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; - } - } - - 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) { - details = hostServices != null ? hostServices.resolveFunction(result.getName()) : null; - 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; - } - - 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 ExpressionNode gatherPrecedence(FHIRLexer lexer, ExpressionNode start, EnumSet ops) { - // work : boolean; - // focus, node, group : ExpressionNode; - - assert(start.isProximal()); - - // is there anything to do? - boolean work = false; - ExpressionNode focus = start.getOpNext(); - if (ops.contains(start.getOperation())) { - while (focus != null && focus.getOperation() != null) { - work = work || !ops.contains(focus.getOperation()); - focus = focus.getOpNext(); - } - } else { - while (focus != null && focus.getOperation() != null) { - work = work || ops.contains(focus.getOperation()); - focus = focus.getOpNext(); - } - } - if (!work) - return start; - - // entry point: tricky - ExpressionNode group; - if (ops.contains(start.getOperation())) { - group = newGroup(lexer, start); - group.setProximal(true); - focus = start; - start = group; - } else { - ExpressionNode node = start; - - focus = node.getOpNext(); - while (!ops.contains(focus.getOperation())) { - node = focus; - focus = focus.getOpNext(); - } - group = newGroup(lexer, focus); - node.setOpNext(group); - } - - // now, at this point: - // group is the group we are adding to, it already has a .group property filled out. - // focus points at the group.group - do { - // run until we find the end of the sequence - while (ops.contains(focus.getOperation())) - focus = focus.getOpNext(); - if (focus.getOperation() != null) { - group.setOperation(focus.getOperation()); - group.setOpNext(focus.getOpNext()); - focus.setOperation(null); - focus.setOpNext(null); - // now look for another sequence, and start it - ExpressionNode node = group; - focus = group.getOpNext(); - 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 - group = newGroup(lexer, focus); - node.setOpNext(group); - } - } - } - } - while (focus != null && focus.getOperation() != null); - return start; - } - - - 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 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++; - } - } - } - - // 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 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 Custom: return checkParamCount(lexer, location, exp, details.getMinParameters(), details.getMaxParameters()); - } - return false; - } - - private List execute(ExecutionContext context, List focus, ExpressionNode exp, boolean atEntry) throws PathEngineException { -// 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); - } - - 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 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) { - List res = new ArrayList(); - res.add(new BooleanType(b)); - 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 { -// System.out.println("Evaluate {'"+exp.toString()+"'} on "+focus.toString()); - TypeDetails result = new TypeDetails(null); - switch (exp.getKind()) { - case Name: - if (atEntry && exp.getName().equals("$this")) - result.update(context.getThisItem()); - 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.addType(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 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); - 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("%context")) - return context.context; - 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("%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); - } - - - 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 + 4; - break; - default: - throw new PathEngineException("Unknown character escape \\"+s.charAt(i)); - } - } else { - b.append(ch); - i++; - } - } - return b.toString(); - } - - - private List operate(List left, Operation operation, List right) throws PathEngineException { - 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 List opAs(List left, List right) { - List result = new ArrayList(); - if (left.size() != 1 || right.size() != 1) - return result; - else { - String tn = convertToString(right); - if (tn.equals(left.get(0).fhirType())) - result.add(left.get(0)); - } - return result; - } - - - 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 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, ""); - 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 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.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") && right.hasType("integer", "decimal")) - return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); - if (left.hasType("date", "dateTime", "time", "instant") && right.hasType("date", "dateTime", "time", "instant")) - return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); - 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); - - 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 PathEngineException { - 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(l.primitiveValue().compareTo(r.primitiveValue()) < 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 PathEngineException("Canonical Comparison isn't done yet"); - } - } - return new ArrayList(); - } - - private List opGreater(List left, List right) throws PathEngineException { - 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")) && (r.hasType("integer", "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(l.primitiveValue().compareTo(r.primitiveValue()) > 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 PathEngineException("Canonical Comparison isn't done yet"); - } - } - return new ArrayList(); - } - - private List opLessOrEqual(List left, List right) throws PathEngineException { - 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")) && (r.hasType("integer", "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(l.primitiveValue().compareTo(r.primitiveValue()) <= 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 PathEngineException("Canonical Comparison isn't done yet"); - } - } - return new ArrayList(); - } - - private List opGreaterOrEqual(List left, List right) throws PathEngineException { - 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")) && (r.hasType("integer", "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(l.primitiveValue().compareTo(r.primitiveValue()) >= 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 PathEngineException("Canonical Comparison isn't done yet"); - } - } - return new ArrayList(); - } - - 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 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()) - 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()).multiply(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 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 makeBoolean(false); - 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())); - 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") && r.hasType("integer", "decimal")) { - 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 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 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 String readConstantType(ExecutionTypeContext context, String constant) throws PathEngineException { - if (constant.equals("true")) - return "boolean"; - else if (constant.equals("false")) - return "boolean"; - else if (Utilities.isInteger(constant)) - return "integer"; - else if (Utilities.isDecimal(constant)) - return "decimal"; - else if (constant.startsWith("%")) - return resolveConstantType(context, constant); - else - return "string"; - } - - private String resolveConstantType(ExecutionTypeContext context, String s) throws PathEngineException { - if (s.equals("%sct")) - return "string"; - else if (s.equals("%loinc")) - return "string"; - else if (s.equals("%ucum")) - return "string"; - else if (s.equals("%context")) - return context.context; - else if (s.equals("%resource")) { - if (context.resource == null) - throw new PathEngineException("%resource cannot be used in this context"); - return context.resource; - } else if (s.equals("%map-codes")) - return "string"; - else if (s.equals("%us-zip")) - return "string"; - else if (s.startsWith("%\"vs-")) - return "string"; - else if (s.startsWith("%\"cs-")) - return "string"; - else if (s.startsWith("%\"ext-")) - return "string"; - else if (hostServices == null) - 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) { - List result = new ArrayList(); - if (atEntry && Character.isUpperCase(exp.getName().charAt(0))) {// special case for start up - if (item instanceof Resource && ((Resource) item).getResourceType().toString().equals(exp.getName())) - result.add(item); - } else - getChildrenByName(item, exp.getName(), result); - return result; - } - - private TypeDetails executeType(String type, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { - if (atEntry && Character.isUpperCase(exp.getName().charAt(0)) && 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; - } - - - @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) - paramTypes.add(executeType(changeThis(context, focus), focus, expr, true)); - else if (exp.getFunction() == Function.Repeat) - ; // it turns out you can't really test this - 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 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()+", not "+focus.describe()); - } - - - 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 PathEngineException { - 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 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 funcAll(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { - 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), false))) { - 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, 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) { - 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) { - 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 PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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(!focus.isEmpty())); // R2 - can't use ElementUtil - return result; - } - - - private List funcResolve(ExecutionContext context, List focus, ExpressionNode exp) { - throw new Error("not Implemented yet"); - } - - private List funcExtension(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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)).matches(sw))); - else - result.add(new BooleanType(false)); - return result; - } - - private List funcContains(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { - 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 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 funcStartsWith(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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 PathEngineException { - 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(focus.isEmpty())); - 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("xhtml")) - return; - String url = null; - if (type.contains(".")) { - url = "http://hl7.org/fhir/StructureDefinition/"+type.substring(0, type.indexOf(".")); - } else { - url = "http://hl7.org/fhir/StructureDefinition/"+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, 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(".")); - } - - 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 = ed.getPath(); - else - tn = t.getCode(); - if (t.getCode().equals("Resource")) { - for (String rn : worker.getResourceNames()) { - if (!result.hasType(worker, rn)) { - result.addType(rn); - getChildTypesByName(rn, "**", result); - } - } - } else if (!result.hasType(worker, tn)) { - result.addType(tn); - getChildTypesByName(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(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); - - if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) - result.addType(path); - else if (t.getCode().equals("Resource")) - result.addTypes(worker.getResourceNames()); - else - result.addType(t.getCode()); - } - } - } - } - } - - private ElementDefinitionMatch getElementDefinition(StructureDefinition sd, String path, boolean allowTypedName) throws PathEngineException { - for (ElementDefinition ed : sd.getSnapshot().getElement()) { - if (ed.getPath().equals(path)) { - if (ed.hasNameReference()) { - return getElementDefinitionByName(sd, ed.getNameReference()); - } 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.hasNameReference() && path.startsWith(ed.getPath()+".")) { - ElementDefinitionMatch m = getElementDefinitionByName(sd, ed.getNameReference()); - return getElementDefinition(sd, m.definition.getPath()+path.substring(ed.getPath().length()), allowTypedName); - } - } - return null; - } - - private boolean isAbstractType(List list) { - return list.size() != 1 ? false : 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 getElementDefinitionByName(StructureDefinition sd, String ref) { - for (ElementDefinition ed : sd.getSnapshot().getElement()) { - if (ref.equals(ed.getName())) - return new ElementDefinitionMatch(ed, null); - } - return null; - } - - - public boolean hasLog() { - return log != null && log.length() > 0; - } - - - public String takeLog() { - if (!hasLog()) - return ""; - String s = log.toString(); - log = new StringBuilder(); - return s; - } - -} \ No newline at end of file +package org.hl7.fhir.instance.utils; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hl7.fhir.instance.model.Base; +import org.hl7.fhir.instance.model.BooleanType; +import org.hl7.fhir.instance.model.DateTimeType; +import org.hl7.fhir.instance.model.DateType; +import org.hl7.fhir.instance.model.DecimalType; +import org.hl7.fhir.instance.model.ElementDefinition; +import org.hl7.fhir.instance.model.ElementDefinition.TypeRefComponent; +import org.hl7.fhir.instance.model.ExpressionNode; +import org.hl7.fhir.instance.model.ExpressionNode.CollectionStatus; +import org.hl7.fhir.instance.model.ExpressionNode.Function; +import org.hl7.fhir.instance.model.ExpressionNode.Kind; +import org.hl7.fhir.instance.model.ExpressionNode.Operation; +import org.hl7.fhir.instance.model.ExpressionNode.SourceLocation; +import org.hl7.fhir.instance.model.ExpressionNode.TypeDetails; +import org.hl7.fhir.instance.model.IntegerType; +import org.hl7.fhir.instance.model.Resource; +import org.hl7.fhir.instance.model.StringType; +import org.hl7.fhir.instance.model.StructureDefinition; +import org.hl7.fhir.instance.model.TemporalPrecisionEnum; +import org.hl7.fhir.instance.model.TimeType; +import org.hl7.fhir.instance.model.Type; +import org.hl7.fhir.instance.utils.FHIRLexer.FHIRLexerException; +import org.hl7.fhir.instance.utils.FHIRPathEngine.IEvaluationContext.FunctionDetails; +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.PathEngineException; +import org.hl7.fhir.exceptions.UcumException; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.ucum.Decimal; + + +/** + * + * @author Grahame Grieve + * + */ +public class FHIRPathEngine { + 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; + } + + } + + public Type resolveConstant(Object appContext, String name); + public String resolveConstantType(Object appContext, String name); + 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); + } + + + /** + * @param worker - used when validating paths (@check), and used doing value set membership when executing tests (once that's defined) + */ + public FHIRPathEngine(IWorkerContext worker) { + super(); + this.worker = worker; + primitiveTypes.add("string"); + primitiveTypes.add("code"); + primitiveTypes.add("integer"); + primitiveTypes.add("boolean"); + primitiveTypes.add("decimal"); + primitiveTypes.add("date"); + primitiveTypes.add("dateTime"); +// for (StructureDefinition sd : worker.allStructures()) { +// if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) +// allTypes.put(sd.getName(), sd); +// if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) { +// primitiveTypes.add(sd.getName()); +// } +// } + } + + + // --- 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 extensing the base model + + public IEvaluationContext getConstantResolver() { + return hostServices; + } + + + public void setConstantResolver(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 + * @ + */ + protected void getChildrenByName(Base item, String name, List result) { + List list = item.listChildrenByName(name); + 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 { + path = path.replace("$.", "$this."); + 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; + } + + /** + * 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 + * @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.contains(".")) + types = new TypeDetails(CollectionStatus.SINGLETON, context); + else { + StructureDefinition sd = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+context.substring(0, context.indexOf('.'))); + 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, 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, String resourceType, String context, String expr) throws FHIRLexerException, PathEngineException, DefinitionException { + return check(appContext, resourceType, context, parse(expr)); + } + + /** + * 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 PathEngineException + * @ + * @ + */ + public List evaluate(Base base, ExpressionNode ExpressionNode) throws PathEngineException { + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(null, null, base, 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 FHIRLexerException + * @throws PathEngineException + * @ + * @ + */ + public List evaluate(Base base, String path) throws FHIRLexerException, PathEngineException { + ExpressionNode exp = parse(path); + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(null, null, base, 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 PathEngineException + * @ + * @ + */ + public List evaluate(Object appContext, Resource resource, Base base, ExpressionNode ExpressionNode) throws PathEngineException { + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(appContext, resource, base, 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 PathEngineException + * @ + * @ + */ + public List evaluate(Object appContext, Base resource, Base base, ExpressionNode ExpressionNode) throws PathEngineException { + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(appContext, resource, base, 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 PathEngineException + * @throws FHIRLexerException + * @ + * @ + */ + public List evaluate(Object appContext, Resource resource, Base base, String path) throws PathEngineException, FHIRLexerException { + ExpressionNode exp = parse(path); + List list = new ArrayList(); + if (base != null) + list.add(base); + log = new StringBuilder(); + return execute(new ExecutionContext(appContext, resource, base, 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 FHIRLexerException + * @throws PathEngineException + * @ + * @ + */ + public boolean evaluateToBoolean(Resource resource, Base base, String path) throws PathEngineException, FHIRLexerException { + 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 + * @param path - the FHIR Path statement to use + * @return + * @throws PathEngineException + * @ + * @ + */ + public boolean evaluateToBoolean(Resource resource, Base base, ExpressionNode node) throws PathEngineException { + return convertToBoolean(evaluate(null, 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 + * @param path - the FHIR Path statement to use + * @return + * @throws PathEngineException + * @ + * @ + */ + public boolean evaluateToBoolean(Base resource, Base base, ExpressionNode node) throws PathEngineException { + 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 FHIRLexerException + * @throws PathEngineException + * @ + * @ + */ + public String evaluateToString(Base base, String path) throws FHIRLexerException, PathEngineException { + return convertToString(evaluate(base, path)); + } + + /** + * 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.getClass().getName(); + } + + /** + * 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; + else + log.append(","); + log.append(convertToString(b)); + } + } + } + + 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; + public ExecutionContext(Object appInfo, Base resource, Base context, Base thisItem) { + this.appInfo = appInfo; + this.resource = resource; + this.context = context; + this.thisItem = thisItem; + } + 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; + } + } + + 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) { + details = hostServices != null ? hostServices.resolveFunction(result.getName()) : null; + 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; + } + + 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 ExpressionNode gatherPrecedence(FHIRLexer lexer, ExpressionNode start, EnumSet ops) { + // work : boolean; + // focus, node, group : ExpressionNode; + + assert(start.isProximal()); + + // is there anything to do? + boolean work = false; + ExpressionNode focus = start.getOpNext(); + if (ops.contains(start.getOperation())) { + while (focus != null && focus.getOperation() != null) { + work = work || !ops.contains(focus.getOperation()); + focus = focus.getOpNext(); + } + } else { + while (focus != null && focus.getOperation() != null) { + work = work || ops.contains(focus.getOperation()); + focus = focus.getOpNext(); + } + } + if (!work) + return start; + + // entry point: tricky + ExpressionNode group; + if (ops.contains(start.getOperation())) { + group = newGroup(lexer, start); + group.setProximal(true); + focus = start; + start = group; + } else { + ExpressionNode node = start; + + focus = node.getOpNext(); + while (!ops.contains(focus.getOperation())) { + node = focus; + focus = focus.getOpNext(); + } + group = newGroup(lexer, focus); + node.setOpNext(group); + } + + // now, at this point: + // group is the group we are adding to, it already has a .group property filled out. + // focus points at the group.group + do { + // run until we find the end of the sequence + while (ops.contains(focus.getOperation())) + focus = focus.getOpNext(); + if (focus.getOperation() != null) { + group.setOperation(focus.getOperation()); + group.setOpNext(focus.getOpNext()); + focus.setOperation(null); + focus.setOpNext(null); + // now look for another sequence, and start it + ExpressionNode node = group; + focus = group.getOpNext(); + 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 + group = newGroup(lexer, focus); + node.setOpNext(group); + } + } + } + } + while (focus != null && focus.getOperation() != null); + return start; + } + + + 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 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++; + } + } + } + + // 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 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 Custom: return checkParamCount(lexer, location, exp, details.getMinParameters(), details.getMaxParameters()); + } + return false; + } + + private List execute(ExecutionContext context, List focus, ExpressionNode exp, boolean atEntry) throws PathEngineException { +// 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); + } + + 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 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) { + List res = new ArrayList(); + res.add(new BooleanType(b)); + 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 { +// System.out.println("Evaluate {'"+exp.toString()+"'} on "+focus.toString()); + TypeDetails result = new TypeDetails(null); + switch (exp.getKind()) { + case Name: + if (atEntry && exp.getName().equals("$this")) + result.update(context.getThisItem()); + 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.addType(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 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); + 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("%context")) + return context.context; + 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("%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); + } + + + 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 + 4; + break; + default: + throw new PathEngineException("Unknown character escape \\"+s.charAt(i)); + } + } else { + b.append(ch); + i++; + } + } + return b.toString(); + } + + + private List operate(List left, Operation operation, List right) throws PathEngineException { + 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 List opAs(List left, List right) { + List result = new ArrayList(); + if (left.size() != 1 || right.size() != 1) + return result; + else { + String tn = convertToString(right); + if (tn.equals(left.get(0).fhirType())) + result.add(left.get(0)); + } + return result; + } + + + 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 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 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.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") && right.hasType("integer", "decimal")) + return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); + if (left.hasType("date", "dateTime", "time", "instant") && right.hasType("date", "dateTime", "time", "instant")) + return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); + 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); + + 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 PathEngineException { + 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(l.primitiveValue().compareTo(r.primitiveValue()) < 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 PathEngineException("Canonical Comparison isn't done yet"); + } + } + return new ArrayList(); + } + + private List opGreater(List left, List right) throws PathEngineException { + 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")) && (r.hasType("integer", "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(l.primitiveValue().compareTo(r.primitiveValue()) > 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 PathEngineException("Canonical Comparison isn't done yet"); + } + } + return new ArrayList(); + } + + private List opLessOrEqual(List left, List right) throws PathEngineException { + 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")) && (r.hasType("integer", "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(l.primitiveValue().compareTo(r.primitiveValue()) <= 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 PathEngineException("Canonical Comparison isn't done yet"); + } + } + return new ArrayList(); + } + + private List opGreaterOrEqual(List left, List right) throws PathEngineException { + 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")) && (r.hasType("integer", "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(l.primitiveValue().compareTo(r.primitiveValue()) >= 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 PathEngineException("Canonical Comparison isn't done yet"); + } + } + return new ArrayList(); + } + + 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 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()) + 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()).multiply(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 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 makeBoolean(false); + 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())); + 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") && r.hasType("integer", "decimal")) { + 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 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 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 String readConstantType(ExecutionTypeContext context, String constant) throws PathEngineException { + if (constant.equals("true")) + return "boolean"; + else if (constant.equals("false")) + return "boolean"; + else if (Utilities.isInteger(constant)) + return "integer"; + else if (Utilities.isDecimal(constant)) + return "decimal"; + else if (constant.startsWith("%")) + return resolveConstantType(context, constant); + else + return "string"; + } + + private String resolveConstantType(ExecutionTypeContext context, String s) throws PathEngineException { + if (s.equals("%sct")) + return "string"; + else if (s.equals("%loinc")) + return "string"; + else if (s.equals("%ucum")) + return "string"; + else if (s.equals("%context")) + return context.context; + else if (s.equals("%resource")) { + if (context.resource == null) + throw new PathEngineException("%resource cannot be used in this context"); + return context.resource; + } else if (s.equals("%map-codes")) + return "string"; + else if (s.equals("%us-zip")) + return "string"; + else if (s.startsWith("%\"vs-")) + return "string"; + else if (s.startsWith("%\"cs-")) + return "string"; + else if (s.startsWith("%\"ext-")) + return "string"; + else if (hostServices == null) + 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) { + List result = new ArrayList(); + if (atEntry && Character.isUpperCase(exp.getName().charAt(0))) {// special case for start up + if (item instanceof Resource && ((Resource) item).getResourceType().toString().equals(exp.getName())) + result.add(item); + } else + getChildrenByName(item, exp.getName(), result); + return result; + } + + private TypeDetails executeType(String type, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { + if (atEntry && Character.isUpperCase(exp.getName().charAt(0)) && 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; + } + + + @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) + paramTypes.add(executeType(changeThis(context, focus), focus, expr, true)); + else if (exp.getFunction() == Function.Repeat) + ; // it turns out you can't really test this + 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 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()+", not "+focus.describe()); + } + + + 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 PathEngineException { + 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 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 funcAll(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { + 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), false))) { + 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, 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) { + 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) { + 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 PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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(!focus.isEmpty())); // R2 - can't use ElementUtil + return result; + } + + + private List funcResolve(ExecutionContext context, List focus, ExpressionNode exp) { + throw new Error("not Implemented yet"); + } + + private List funcExtension(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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)).matches(sw))); + else + result.add(new BooleanType(false)); + return result; + } + + private List funcContains(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { + 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 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 funcStartsWith(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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 PathEngineException { + 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(focus.isEmpty())); + 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("xhtml")) + return; + String url = null; + if (type.contains(".")) { + url = "http://hl7.org/fhir/StructureDefinition/"+type.substring(0, type.indexOf(".")); + } else { + url = "http://hl7.org/fhir/StructureDefinition/"+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, 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(".")); + } + + 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 = ed.getPath(); + else + tn = t.getCode(); + if (t.getCode().equals("Resource")) { + for (String rn : worker.getResourceNames()) { + if (!result.hasType(worker, rn)) { + result.addType(rn); + getChildTypesByName(rn, "**", result); + } + } + } else if (!result.hasType(worker, tn)) { + result.addType(tn); + getChildTypesByName(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(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); + + if (t.getCode().equals("Element") || t.getCode().equals("BackboneElement")) + result.addType(path); + else if (t.getCode().equals("Resource")) + result.addTypes(worker.getResourceNames()); + else + result.addType(t.getCode()); + } + } + } + } + } + + private ElementDefinitionMatch getElementDefinition(StructureDefinition sd, String path, boolean allowTypedName) throws PathEngineException { + for (ElementDefinition ed : sd.getSnapshot().getElement()) { + if (ed.getPath().equals(path)) { + if (ed.hasNameReference()) { + return getElementDefinitionByName(sd, ed.getNameReference()); + } 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.hasNameReference() && path.startsWith(ed.getPath()+".")) { + ElementDefinitionMatch m = getElementDefinitionByName(sd, ed.getNameReference()); + return getElementDefinition(sd, m.definition.getPath()+path.substring(ed.getPath().length()), allowTypedName); + } + } + return null; + } + + private boolean isAbstractType(List list) { + return list.size() != 1 ? false : 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 getElementDefinitionByName(StructureDefinition sd, String ref) { + for (ElementDefinition ed : sd.getSnapshot().getElement()) { + if (ref.equals(ed.getName())) + return new ElementDefinitionMatch(ed, null); + } + return null; + } + + + public boolean hasLog() { + return log != null && log.length() > 0; + } + + + public String takeLog() { + if (!hasLog()) + return ""; + String s = log.toString(); + log = new StringBuilder(); + return s; + } + +} diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/ProfileUtilities.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/ProfileUtilities.java index a82e9b20cc7..9d2c0a2ae41 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/ProfileUtilities.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/utils/ProfileUtilities.java @@ -1628,7 +1628,7 @@ public class ProfileUtilities { if (definition != null && definition.hasShort()) { if (!c.getPieces().isEmpty()) c.addPiece(gen.new Piece("br")); c.addPiece(checkForNoChange(definition.getShortElement(), gen.new Piece(null, definition.getShort(), null))); - } else if (fallback != null && fallback != null && fallback.hasShort()) { + } else if (fallback != null && fallback.hasShort()) { if (!c.getPieces().isEmpty()) c.addPiece(gen.new Piece("br")); c.addPiece(checkForNoChange(fallback.getShortElement(), gen.new Piece(null, fallback.getShort(), null))); } 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 df0e5b5c9fb..c72ec382d36 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,27 +1,56 @@ package org.hl7.fhir.r4.utils; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + import ca.uhn.fhir.model.api.TemporalPrecisionEnum; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.util.ElementUtil; -import org.fhir.ucum.Decimal; -import org.fhir.ucum.UcumException; -import org.hl7.fhir.exceptions.DefinitionException; -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.r4.conformance.ProfileUtilities; import org.hl7.fhir.r4.context.IWorkerContext; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.elementmodel.Element; +import org.hl7.fhir.r4.elementmodel.ObjectConverter; +import org.hl7.fhir.r4.model.Base; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.ElementDefinition; import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent; -import org.hl7.fhir.r4.model.ExpressionNode.*; +import org.hl7.fhir.r4.model.ExpressionNode; +import org.hl7.fhir.r4.model.ExpressionNode.CollectionStatus; +import org.hl7.fhir.r4.model.ExpressionNode.Function; +import org.hl7.fhir.r4.model.ExpressionNode.Kind; +import org.hl7.fhir.r4.model.ExpressionNode.Operation; +import org.hl7.fhir.r4.model.ExpressionNode.SourceLocation; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Property; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule; +import org.hl7.fhir.r4.model.TimeType; +import org.hl7.fhir.r4.model.TypeDetails; +import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.TypeDetails.ProfiledType; import org.hl7.fhir.r4.utils.FHIRLexer.FHIRLexerException; import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext.FunctionDetails; +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.utilities.Utilities; +import org.fhir.ucum.Decimal; +import org.fhir.ucum.UcumException; -import java.math.BigDecimal; -import java.util.*; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.ElementUtil; /** * @@ -35,6 +64,88 @@ 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) */ @@ -50,39 +161,93 @@ 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 - private ExecutionContext changeThis(ExecutionContext context, Base newThis) { - return new ExecutionContext(context.appInfo, context.resource, context.context, context.aliases, newThis); + public IEvaluationContext getHostServices() { + return hostServices; } - private ExecutionTypeContext changeThis(ExecutionTypeContext context, TypeDetails newThis) { - return new ExecutionTypeContext(context.appInfo, context.resource, context.context, 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; } /** * 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 - * @throws DefinitionException - * @throws PathEngineException + * @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 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(".")) { @@ -94,18 +259,18 @@ public class FHIRPathEngine { ctxt = resourceType.substring(0, resourceType.lastIndexOf("/")+1)+ctxt; } StructureDefinition sd = worker.fetchResource(StructureDefinition.class, ctxt); - if (sd == null) + if (sd == null) throw new PathEngineException("Unknown context "+context); ElementDefinitionMatch ed = getElementDefinition(sd, context, true); - if (ed == null) + if (ed == null) throw new PathEngineException("Unknown context element "+context); - if (ed.fixedType != null) + if (ed.fixedType != null) types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); - else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) + 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()) + for (TypeRefComponent t : ed.getDefinition().getType()) types.addType(t.getCode()); } } @@ -113,24 +278,22 @@ public class FHIRPathEngine { 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; + // 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) + if (ed == null) throw new PathEngineException("Unknown context element "+context); - if (ed.fixedType != null) + if (ed.fixedType != null) types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); - else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) + 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()); } } @@ -139,7 +302,7 @@ 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); } @@ -148,6 +311,477 @@ public class FHIRPathEngine { return check(appContext, resourceType, context, parse(expr)); } + + /** + * 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); + } + + /** + * 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 + * @param path - the FHIR Path statement to use + * @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 + * @param path - the FHIR Path statement to use + * @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 + * @param path - the FHIR Path statement to use + * @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)); + } + + /** + * 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; + else + log.append(","); + log.append(convertToString(b)); + } + } + } + + 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(); + } + 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 ExpressionNode gatherPrecedence(FHIRLexer lexer, ExpressionNode start, EnumSet ops) { + // work : boolean; + // focus, node, group : ExpressionNode; + + assert(start.isProximal()); + + // is there anything to do? + boolean work = false; + ExpressionNode focus = start.getOpNext(); + if (ops.contains(start.getOperation())) { + while (focus != null && focus.getOperation() != null) { + work = work || !ops.contains(focus.getOperation()); + focus = focus.getOpNext(); + } + } else { + while (focus != null && focus.getOperation() != null) { + work = work || ops.contains(focus.getOperation()); + focus = focus.getOpNext(); + } + } + if (!work) + return start; + + // entry point: tricky + ExpressionNode group; + if (ops.contains(start.getOperation())) { + group = newGroup(lexer, start); + group.setProximal(true); + focus = start; + start = group; + } else { + ExpressionNode node = start; + + focus = node.getOpNext(); + while (!ops.contains(focus.getOperation())) { + node = focus; + focus = focus.getOpNext(); + } + group = newGroup(lexer, focus); + node.setOpNext(group); + } + + // now, at this point: + // group is the group we are adding to, it already has a .group property filled out. + // focus points at the group.group + do { + // run until we find the end of the sequence + while (ops.contains(focus.getOperation())) + focus = focus.getOpNext(); + if (focus.getOperation() != null) { + group.setOperation(focus.getOperation()); + group.setOpNext(focus.getOpNext()); + focus.setOperation(null); + focus.setOpNext(null); + // now look for another sequence, and start it + ExpressionNode node = group; + focus = group.getOpNext(); + 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 + group = newGroup(lexer, focus); + node.setOpNext(group); + } + } + } + } + while (focus != null && focus.getOperation() != null); + return start; + } + + + 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 void checkConstant(String s, FHIRLexer lexer) throws FHIRLexerException { if (s.startsWith("\'") && s.endsWith("\'")) { int i = 1; @@ -155,14 +789,14 @@ public class FHIRPathEngine { char ch = s.charAt(i); if (ch == '\\') { switch (ch) { - case 't': + case 't': case 'r': - case 'n': - case 'f': + case 'n': + case 'f': case '\'': - case '\\': - case '/': - i++; + case '\\': + case '/': + i++; break; case 'u': if (!Utilities.isHex("0x"+s.substring(i, i+4))) @@ -177,30 +811,11 @@ public class FHIRPathEngine { } } - 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"); - } + // 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) @@ -214,20 +829,6 @@ public class FHIRPathEngine { 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); @@ -279,542 +880,6 @@ public class FHIRPathEngine { 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; - } - - /** - * 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 Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); - 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 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); - } - - /** - * 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()); - } - for (ElementDefinition t : childDefinitions) { - if (tailMatches(t, expr.getName())) { - focus = t; - break; - } - } - } 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 (!"Reference".equals(element.getType().get(0).getCode())) - throw new DefinitionException("illegal use of resolve() in discriminator - type on "+element.getId()+" is not Reference ("+element.getType().get(0).getCode()+")"); - sd = worker.fetchResource(StructureDefinition.class, element.getType().get(0).getTargetProfile()); - 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())) - throw new DefinitionException("Not supported yet"); - 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)"); - } - - 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 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 - * @ - */ - 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) - * - * @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, 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 - * @param path - the FHIR Path statement to use - * @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 - * @param path - the FHIR Path statement to use - * @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)); - } - 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(); @@ -828,7 +893,7 @@ public class FHIRPathEngine { for (Base base : outcome) if (base != null) work.add(base); - } + } break; case Function: List work2 = evaluateFunction(context, focus, exp); @@ -870,26 +935,34 @@ public class FHIRPathEngine { return work; } - private List execute(ExecutionContext context, Base item, ExpressionNode exp, boolean atEntry) throws FHIRException { + private List executeTypeName(ExecutionContext context, List focus, ExpressionNode next, boolean atEntry) { 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.add(new StringType(next.getName())); 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 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) { + List res = new ArrayList(); + res.add(new BooleanType(b)); + 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 { @@ -941,6 +1014,815 @@ public class FHIRPathEngine { 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); + 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 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 List opAs(List left, List right) { + List result = new ArrayList(); + if (left.size() != 1 || right.size() != 1) + return result; + else { + String tn = convertToString(right); + if (tn.equals(left.get(0).fhirType())) + result.add(left.get(0)); + } + return result; + } + + + 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 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 MemberOf: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + case Contains: return new TypeDetails(CollectionStatus.SINGLETON, "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.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 Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); + 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); + + 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(l.primitiveValue().compareTo(r.primitiveValue()) < 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(l.primitiveValue().compareTo(r.primitiveValue()) > 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(l.primitiveValue().compareTo(r.primitiveValue()) <= 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(l.primitiveValue().compareTo(r.primitiveValue()) >= 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 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) { + boolean f = false; + for (Base l : left) + if (doEquals(l, r)) { + f = true; + break; + } + if (!f) { + ans = false; + break; + } + } + return makeBoolean(ans); + } + + 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()) + 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()).multiply(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 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())); + 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 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 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"); + else if (constant.equals("false")) + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + else if (Utilities.isInteger(constant)) + return new TypeDetails(CollectionStatus.SINGLETON, "integer"); + else if (Utilities.isDecimal(constant)) + return new TypeDetails(CollectionStatus.SINGLETON, "decimal"); + else if (constant.startsWith("%")) + return resolveConstantType(context, constant); + else + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + } + + private TypeDetails resolveConstantType(ExecutionTypeContext context, String s) throws PathEngineException { + if (s.equals("%sct")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.equals("%loinc")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.equals("%ucum")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.equals("%resource")) { + if (context.resource == null) + throw new PathEngineException("%resource cannot be used in this context"); + return new TypeDetails(CollectionStatus.SINGLETON, context.resource); + } else if (s.equals("%context")) { + return new TypeDetails(CollectionStatus.SINGLETON, context.context); + } else if (s.equals("%map-codes")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.equals("%us-zip")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.startsWith("%\"vs-")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.startsWith("%\"cs-")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (s.startsWith("%\"ext-")) + return new TypeDetails(CollectionStatus.SINGLETON, "string"); + else if (hostServices == null) + 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)) && hashTail(type).equals(exp.getName())) // special case for start up return new TypeDetails(CollectionStatus.SINGLETON, type); @@ -949,34 +1831,311 @@ public class FHIRPathEngine { return result; } - private List executeTypeName(ExecutionContext context, List focus, ExpressionNode next, boolean atEntry) { - List result = new ArrayList(); - result.add(new StringType(next.getName())); + + 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, "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 executeTypeName(ExecutionTypeContext context, TypeDetails focus, ExpressionNode exp, boolean atEntry) throws PathEngineException, DefinitionException { - return new TypeDetails(CollectionStatus.SINGLETON, exp.getName()); + private TypeDetails anything(CollectionStatus status) { + return new TypeDetails(status, allTypes.keySet()); } - 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.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()); - else - return worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + ed.getType().get(0).getCode()); + // 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"); + } } - public String forLog() { - if (log.length() > 0) - return " ("+log.toString()+")"; - else - return ""; + 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 { @@ -987,14 +2146,7 @@ public class FHIRPathEngine { 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 { @@ -1019,7 +2171,7 @@ public class FHIRPathEngine { boolean v = false; if (item instanceof BooleanType) { v = ((BooleanType) item).booleanValue(); - } else + } else v = item != null; if (!v) { all = false; @@ -1031,42 +2183,34 @@ public class FHIRPathEngine { } } - private List funcAs(ExecutionContext context, List focus, ExpressionNode exp) { + + 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(); - String tn = exp.getParameters().get(0).getName(); - for (Base b : focus) - if (b.hasType(tn)) - result.add(b); + result.add(DateTimeType.now()); return result; } - private List funcChildren(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + + private List funcToday(ExecutionContext context, List focus, ExpressionNode exp) { List result = new ArrayList(); - for (Base b : focus) - getChildrenByName(b, "*", result); + result.add(new DateType(new Date(), TemporalPrecisionEnum.DAY)); 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 funcMemberOf(ExecutionContext context, List focus, ExpressionNode exp) { + throw new Error("not Implemented yet"); } - 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(); @@ -1087,31 +2231,32 @@ public class FHIRPathEngine { return result; } - private List funcDistinct(ExecutionContext context, List focus, ExpressionNode exp) { - if (focus.size() <= 1) - return focus; + private List funcChildren(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { 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)); - } + for (Base b : focus) + getChildrenByName(b, "*", result); return result; } - private List funcEmpty(ExecutionContext context, List focus, ExpressionNode exp) { + + 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(); - result.add(new BooleanType(ElementUtil.isEmpty(focus))); + 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)); @@ -1123,46 +2268,22 @@ public class FHIRPathEngine { return result; } - private List funcExists(ExecutionContext context, List focus, ExpressionNode exp) { + + private List funcToString(ExecutionContext context, List focus, ExpressionNode exp) { List result = new ArrayList(); - result.add(new BooleanType(!ElementUtil.isEmpty(focus))); + result.add(new StringType(convertToString(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); - } - } + 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 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); @@ -1176,9 +2297,28 @@ public class FHIRPathEngine { 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) + if (focus.size() == 0 || focus.size() > 1) result.add(new BooleanType(false)); else { String tn = exp.getParameters().get(0).getName(); @@ -1187,74 +2327,16 @@ public class FHIRPathEngine { 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 { + private List funcAs(ExecutionContext context, List focus, ExpressionNode exp) { 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))); + String tn = exp.getParameters().get(0).getName(); + for (Base b : focus) + if (b.hasType(tn)) + result.add(b); 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(); @@ -1278,21 +2360,78 @@ public class FHIRPathEngine { return result; } - private List funcReplace(ExecutionContext context, List focus, ExpressionNode exp) { - throw new Error("not Implemented yet"); + + + 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 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)); + 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) { @@ -1326,30 +2465,97 @@ public class FHIRPathEngine { return result; } - private List funcSelect(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { + private List funcExtension(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { List result = new ArrayList(); - List pc = new ArrayList(); + List nl = execute(context, focus, exp.getParameters().get(0), true); + String url = nl.get(0).primitiveValue(); + for (Base item : focus) { - pc.clear(); - pc.add(item); - result.addAll(execute(changeThis(context, item), pc, exp.getParameters().get(0), true)); + 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 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 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 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()); + private List funcDistinct(ExecutionContext context, List focus, ExpressionNode exp) { + if (focus.size() <= 1) + return focus; List result = new ArrayList(); - for (int i = i1; i < focus.size(); i++) - result.add(focus.get(i)); + 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; } @@ -1364,28 +2570,6 @@ public class FHIRPathEngine { 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); @@ -1405,59 +2589,12 @@ public class FHIRPathEngine { s = sw.substring(i1, Math.min(sw.length(), i1+i2)); else s = sw.substring(i1); - if (!Utilities.noString(s)) + 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(); @@ -1466,26 +2603,44 @@ public class FHIRPathEngine { return result; } - private List funcToString(ExecutionContext context, List focus, ExpressionNode exp) { + private List funcCount(ExecutionContext context, List focus, ExpressionNode exp) { List result = new ArrayList(); - result.add(new StringType(convertToString(focus))); + result.add(new IntegerType(focus.size())); return result; } - private List funcToday(ExecutionContext context, List focus, ExpressionNode exp) { + 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(); - result.add(new DateType(new Date(), TemporalPrecisionEnum.DAY)); + for (int i = i1; i < focus.size(); i++) + result.add(focus.get(i)); 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 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(); @@ -1498,82 +2653,52 @@ public class FHIRPathEngine { return result; } - private ExpressionNode gatherPrecedence(FHIRLexer lexer, ExpressionNode start, EnumSet ops) { - // work : boolean; - // focus, node, group : ExpressionNode; - - assert(start.isProximal()); - - // is there anything to do? - boolean work = false; - ExpressionNode focus = start.getOpNext(); - if (ops.contains(start.getOperation())) { - while (focus != null && focus.getOperation() != null) { - work = work || !ops.contains(focus.getOperation()); - focus = focus.getOpNext(); - } - } else { - while (focus != null && focus.getOperation() != null) { - work = work || ops.contains(focus.getOperation()); - focus = focus.getOpNext(); - } + 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)); } - if (!work) - return start; - - // entry point: tricky - ExpressionNode group; - if (ops.contains(start.getOperation())) { - group = newGroup(lexer, start); - group.setProximal(true); - focus = start; - start = group; - } else { - ExpressionNode node = start; - - focus = node.getOpNext(); - while (!ops.contains(focus.getOperation())) { - node = focus; - focus = focus.getOpNext(); - } - group = newGroup(lexer, focus); - node.setOpNext(group); - } - - // now, at this point: - // group is the group we are adding to, it already has a .group property filled out. - // focus points at the group.group - do { - // run until we find the end of the sequence - while (ops.contains(focus.getOperation())) - focus = focus.getOpNext(); - if (focus.getOperation() != null) { - group.setOperation(focus.getOperation()); - group.setOpNext(focus.getOpNext()); - focus.setOperation(null); - focus.setOpNext(null); - // now look for another sequence, and start it - ExpressionNode node = group; - focus = group.getOpNext(); - 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 - group = newGroup(lexer, focus); - node.setOpNext(group); - } - } - } - } - while (focus != null && focus.getOperation() != null); - return start; + 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 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)) @@ -1633,14 +2758,14 @@ public class FHIRPathEngine { 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()) { @@ -1686,28 +2811,6 @@ public class FHIRPathEngine { } } - /** - * 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 ElementDefinitionMatch getElementDefinition(StructureDefinition sd, String path, boolean allowTypedName) throws PathEngineException { for (ElementDefinition ed : sd.getSnapshot().getElement()) { if (ed.getPath().equals(path)) { @@ -1725,12 +2828,12 @@ public class FHIRPathEngine { 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())) { + 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) + if (nsd == null) throw new PathEngineException("Unknown type "+ed.getType().get(0).getCode()); return getElementDefinition(nsd, nsd.getId()+path.substring(ed.getPath().length()), allowTypedName); } @@ -1742,988 +2845,35 @@ public class FHIRPathEngine { 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; - } + private boolean isAbstractType(List list) { + return list.size() != 1 ? true : Utilities.existsInList(list.get(0).getCode(), "Element", "BackboneElement", "Resource", "DomainResource"); +} - public IEvaluationContext getHostServices() { - return hostServices; - } - public void setHostServices(IEvaluationContext constantResolver) { - this.hostServices = constantResolver; + 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; } - private boolean hasType(ElementDefinition ed, String s) { - for (TypeRefComponent t : ed.getType()) - if (s.equalsIgnoreCase(t.getCode())) - return true; - return false; - } - - private String hashTail(String type) { - return type.contains("#") ? "" : type.substring(type.lastIndexOf("/")+1); - } - - private boolean isAbstractType(List list) { - return list.size() != 1 ? true : Utilities.existsInList(list.get(0).getCode(), "Element", "BackboneElement", "Resource", "DomainResource"); -} - - 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)); - } - } - } - - private List makeBoolean(boolean b) { - List res = new ArrayList(); - res.add(new BooleanType(b)); - return res; - } - - 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 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 List opAs(List left, List right) { - List result = new ArrayList(); - if (left.size() != 1 || right.size() != 1) - return result; - else { - String tn = convertToString(right); - if (tn.equals(left.get(0).fhirType())) - result.add(left.get(0)); - } - 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 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()) - 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(l.primitiveValue().compareTo(r.primitiveValue()) > 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(l.primitiveValue().compareTo(r.primitiveValue()) >= 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) 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)); - 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(l.primitiveValue().compareTo(r.primitiveValue()) <= 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(l.primitiveValue().compareTo(r.primitiveValue()) < 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 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 -: 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); - - 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()) - 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()).multiply(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 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, "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, ""); - 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 MemberOf: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Contains: return new TypeDetails(CollectionStatus.SINGLETON, "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; - } - - /** - * 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 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; - } - - 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++; - } - } - return b.toString(); - } - - 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 - return new DateType(value); - } - - private TypeDetails readConstantType(ExecutionTypeContext context, String constant) throws PathEngineException { - if (constant.equals("true")) - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - else if (constant.equals("false")) - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - else if (Utilities.isInteger(constant)) - return new TypeDetails(CollectionStatus.SINGLETON, "integer"); - else if (Utilities.isDecimal(constant)) - return new TypeDetails(CollectionStatus.SINGLETON, "decimal"); - else if (constant.startsWith("%")) - return resolveConstantType(context, constant); - else - 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"); - else if (s.equals("%loinc")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.equals("%ucum")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.equals("%resource")) { - if (context.resource == null) - throw new PathEngineException("%resource cannot be used in this context"); - return new TypeDetails(CollectionStatus.SINGLETON, context.resource); - } else if (s.equals("%context")) { - return new TypeDetails(CollectionStatus.SINGLETON, context.context); - } else if (s.equals("%map-codes")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.equals("%us-zip")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.startsWith("%\"vs-")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.startsWith("%\"cs-")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (s.startsWith("%\"ext-")) - return new TypeDetails(CollectionStatus.SINGLETON, "string"); - else if (hostServices == null) - throw new PathEngineException("Unknown fixed constant type for '"+s+"'"); - else - return hostServices.resolveConstantType(context.appInfo, s); - } - - 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 - return tail.equals(d); - } public String takeLog() { if (!hasLog()) @@ -2734,167 +2884,107 @@ public class FHIRPathEngine { } - // 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; + /** 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; - /** - * @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; + 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()); } - public String getDescription() { - return description; + for (ElementDefinition t : childDefinitions) { + if (tailMatches(t, expr.getName())) { + focus = t; + break; + } } - - public int getMaxParameters() { - return maxParameters; - } - - public int getMinParameters() { - return minParameters; - } - + } 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 (!"Reference".equals(element.getType().get(0).getCode())) + throw new DefinitionException("illegal use of resolve() in discriminator - type on "+element.getId()+" is not Reference ("+element.getType().get(0).getCode()+")"); + sd = worker.fetchResource(StructureDefinition.class, element.getType().get(0).getTargetProfile()); + 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(); + 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()); + 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)"); } + 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 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 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.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()); + else + return worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + ed.getType().get(0).getCode()); } - 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 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 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 String tailDot(String path) { + return path.substring(path.lastIndexOf(".") + 1); } } diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/OperationOutcomeUtilities.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/OperationOutcomeUtilities.java index f456378def6..280d97653c5 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/OperationOutcomeUtilities.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/OperationOutcomeUtilities.java @@ -35,6 +35,7 @@ public class OperationOutcomeUtilities { case ERROR : return IssueSeverity.ERROR; case WARNING : return IssueSeverity.WARNING; case INFORMATION : return IssueSeverity.INFORMATION; + case NULL : return IssueSeverity.NULL; } return IssueSeverity.NULL; } @@ -70,6 +71,7 @@ public class OperationOutcomeUtilities { case TIMEOUT: return IssueType.TIMEOUT; case THROTTLED: return IssueType.THROTTLED; case INFORMATIONAL: return IssueType.INFORMATIONAL; + case NULL: return IssueType.NULL; } return IssueType.NULL; } diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/QuestionnaireBuilder.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/QuestionnaireBuilder.java index 1d1481664a0..5132e96f21a 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/QuestionnaireBuilder.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/QuestionnaireBuilder.java @@ -589,20 +589,20 @@ public class QuestionnaireBuilder { private Type convertType(Base value, QuestionnaireItemType af, ValueSet vs, String path) throws FHIRException { switch (af) { // simple cases - case BOOLEAN: if (value instanceof BooleanType) return (Type) value; - case DECIMAL: if (value instanceof DecimalType) return (Type) value; - case INTEGER: if (value instanceof IntegerType) return (Type) value; - case DATE: if (value instanceof DateType) return (Type) value; - case DATETIME: if (value instanceof DateTimeType) return (Type) value; - case TIME: if (value instanceof TimeType) return (Type) value; + case BOOLEAN: if (value instanceof BooleanType) return (Type) value; break; + case DECIMAL: if (value instanceof DecimalType) return (Type) value; break; + case INTEGER: if (value instanceof IntegerType) return (Type) value; break; + case DATE: if (value instanceof DateType) return (Type) value; break; + case DATETIME: if (value instanceof DateTimeType) return (Type) value; break; + case TIME: if (value instanceof TimeType) return (Type) value; break; case STRING: if (value instanceof StringType) return (Type) value; else if (value instanceof UriType) return new StringType(((UriType) value).asStringValue()); - - case TEXT: if (value instanceof StringType) return (Type) value; - case QUANTITY: if (value instanceof Quantity) return (Type) value; + break; + case TEXT: if (value instanceof StringType) return (Type) value; break; + case QUANTITY: if (value instanceof Quantity) return (Type) value; break; // complex cases: // ? QuestionnaireItemTypeAttachment: ...? @@ -621,6 +621,7 @@ public class QuestionnaireBuilder { cc.setSystem(getSystemForCode(vs, cc.getCode(), path)); return cc; } + break; case REFERENCE: if (value instanceof Reference) @@ -629,6 +630,9 @@ public class QuestionnaireBuilder { Reference r = new Reference(); r.setReference(((StringType) value).asStringValue()); } + break; + default: + break; } throw new FHIRException("Unable to convert from '"+value.getClass().toString()+"' for Answer Format "+af.toCode()+", path = "+path); diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/StructureMapUtilities.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/StructureMapUtilities.java index 388421225c6..8cd8f23fa9e 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/StructureMapUtilities.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/StructureMapUtilities.java @@ -365,11 +365,14 @@ public class StructureMapUtilities { private static void renderGroup(StringBuilder b, StructureMapGroupComponent g) { b.append("group "); switch (g.getTypeMode()) { - case TYPES: b.append("for types"); - case TYPEANDTYPES: b.append("for type+types "); + case TYPES: + b.append("for types"); + break; + case TYPEANDTYPES: + b.append("for type+types "); + break; default: // NONE, NULL } - b.append("for types "); b.append(g.getName()); if (g.hasExtends()) { b.append(" extends "); diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/formats/JsonTrackingParser.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/formats/JsonTrackingParser.java index 0ea6a5a8e06..5b0ea3e79fb 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/formats/JsonTrackingParser.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/formats/JsonTrackingParser.java @@ -364,6 +364,9 @@ public class JsonTrackingParser { break; case Eof : throw lexer.error("Unexpected End of File"); + case End: + // TODO GG: This isn't handled. Should it be? + break; } next(); } @@ -405,6 +408,10 @@ public class JsonTrackingParser { break; case Eof : throw lexer.error("Unexpected End of File"); + case End: + case Boolean: + // TODO GG: These aren't handled. SHould they be? + break; } next(); } diff --git a/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/test/support/TestingUtilities.java b/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/test/support/TestingUtilities.java index e55b9fa2206..1d9860f59a6 100644 --- a/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/test/support/TestingUtilities.java +++ b/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/test/support/TestingUtilities.java @@ -270,7 +270,7 @@ public class TestingUtilities { JsonArray a1 = (JsonArray) n1; JsonArray a2 = (JsonArray) n2; - if (a1.size() != a1.size()) + if (a1.size() != a2.size()) return "array properties differ at "+path+": count "+Integer.toString(a1.size())+"/"+Integer.toString(a2.size()); for (int i = 0; i < a1.size(); i++) { String s = compareNodes(path+"["+Integer.toString(i)+"]", a1.get(i), a2.get(i)); diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java index 0c1f632ae54..8e1c430e1ee 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java @@ -1,24 +1,31 @@ package ca.uhn.fhir.to; -import static org.apache.commons.lang3.StringUtils.defaultString; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.*; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.model.api.ExtensionDt; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu2.resource.Conformance; +import ca.uhn.fhir.model.primitive.DecimalDt; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.client.api.IClientInterceptor; +import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.IHttpResponse; +import ca.uhn.fhir.rest.client.impl.GenericClient; +import ca.uhn.fhir.to.model.HomeRequest; +import ca.uhn.fhir.util.ExtensionConstants; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.Header; import org.apache.http.entity.ContentType; import org.apache.http.message.BasicHeader; -import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.CapabilityStatement; import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestComponent; import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceComponent; +import org.hl7.fhir.dstu3.model.DecimalType; +import org.hl7.fhir.dstu3.model.Extension; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IDomainResource; @@ -26,23 +33,19 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ui.ModelMap; import org.thymeleaf.TemplateEngine; -import ca.uhn.fhir.context.*; -import ca.uhn.fhir.model.api.ExtensionDt; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.dstu2.resource.Conformance; -import ca.uhn.fhir.model.primitive.DecimalDt; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.client.api.*; -import ca.uhn.fhir.rest.client.impl.GenericClient; -import ca.uhn.fhir.to.model.HomeRequest; -import ca.uhn.fhir.util.ExtensionConstants; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.*; + +import static org.apache.commons.lang3.StringUtils.defaultString; public class BaseController { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseController.class); static final String PARAM_RESOURCE = "resource"; static final String RESOURCE_COUNT_EXT_URL = "http://hl7api.sourceforge.net/hapi-fhir/res/extdefs.html#resourceCount"; - + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseController.class); @Autowired protected TesterConfig myConfig; private Map myContexts = new HashMap(); @@ -294,13 +297,15 @@ public class BaseController { private IBaseResource loadAndAddConf(HttpServletRequest theServletRequest, final HomeRequest theRequest, final ModelMap theModel) { switch (theRequest.getFhirVersion(myConfig)) { - case DSTU2: - return loadAndAddConfDstu2(theServletRequest, theRequest, theModel); - case DSTU3: - return loadAndAddConfDstu3(theServletRequest, theRequest, theModel); - case DSTU2_1: - case DSTU2_HL7ORG: - break; + case DSTU2: + return loadAndAddConfDstu2(theServletRequest, theRequest, theModel); + case DSTU3: + return loadAndAddConfDstu3(theServletRequest, theRequest, theModel); + case R4: + return loadAndAddConfR4(theServletRequest, theRequest, theModel); + case DSTU2_1: + case DSTU2_HL7ORG: + break; } throw new IllegalStateException("Unknown version: " + theRequest.getFhirVersion(myConfig)); } @@ -379,7 +384,7 @@ public class BaseController { } theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement)); - + Map resourceCounts = new HashMap(); long total = 0; @@ -427,6 +432,67 @@ public class BaseController { return capabilityStatement; } + private IBaseResource loadAndAddConfR4(HttpServletRequest theServletRequest, final HomeRequest theRequest, final ModelMap theModel) { + CaptureInterceptor interceptor = new CaptureInterceptor(); + GenericClient client = theRequest.newClient(theServletRequest, getContext(theRequest), myConfig, interceptor); + + org.hl7.fhir.r4.model.CapabilityStatement capabilityStatement = new org.hl7.fhir.r4.model.CapabilityStatement(); + try { + capabilityStatement = client.fetchConformance().ofType(org.hl7.fhir.r4.model.CapabilityStatement.class).execute(); + } catch (Exception ex) { + ourLog.warn("Failed to load conformance statement, error was: {}", ex.toString()); + theModel.put("errorMsg", toDisplayError("Failed to load conformance statement, error was: " + ex.toString(), ex)); + } + + theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement)); + + Map resourceCounts = new HashMap(); + long total = 0; + + for (org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) { + for (org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent nextResource : nextRest.getResource()) { + List exts = nextResource.getExtensionsByUrl(RESOURCE_COUNT_EXT_URL); + if (exts != null && exts.size() > 0) { + Number nextCount = ((org.hl7.fhir.r4.model.DecimalType) (exts.get(0).getValue())).getValueAsNumber(); + resourceCounts.put(nextResource.getTypeElement().getValue(), nextCount); + total += nextCount.longValue(); + } + } + } + + theModel.put("resourceCounts", resourceCounts); + + if (total > 0) { + for (org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) { + Collections.sort(nextRest.getResource(), new Comparator() { + @Override + public int compare(org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent theO1, org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent theO2) { + org.hl7.fhir.r4.model.DecimalType count1 = new org.hl7.fhir.r4.model.DecimalType(); + List count1exts = theO1.getExtensionsByUrl(RESOURCE_COUNT_EXT_URL); + if (count1exts != null && count1exts.size() > 0) { + count1 = (org.hl7.fhir.r4.model.DecimalType) count1exts.get(0).getValue(); + } + org.hl7.fhir.r4.model.DecimalType count2 = new org.hl7.fhir.r4.model.DecimalType(); + List count2exts = theO2.getExtensionsByUrl(RESOURCE_COUNT_EXT_URL); + if (count2exts != null && count2exts.size() > 0) { + count2 = (org.hl7.fhir.r4.model.DecimalType) count2exts.get(0).getValue(); + } + int retVal = count2.compareTo(count1); + if (retVal == 0) { + retVal = theO1.getTypeElement().getValue().compareTo(theO2.getTypeElement().getValue()); + } + return retVal; + } + }); + } + } + + theModel.put("requiredParamExtension", ExtensionConstants.PARAM_IS_REQUIRED); + + theModel.put("conf", capabilityStatement); + return capabilityStatement; + } + protected String logPrefix(ModelMap theModel) { return "[server=" + theModel.get("serverId") + "] - "; } @@ -485,7 +551,7 @@ public class BaseController { } protected void processAndAddLastClientInvocation(GenericClient theClient, ResultType theResultType, ModelMap theModelMap, long theLatency, String outcomeDescription, - CaptureInterceptor theInterceptor, HomeRequest theRequest) { + CaptureInterceptor theInterceptor, HomeRequest theRequest) { try { // ApacheHttpRequest lastRequest = theInterceptor.getLastRequest(); // HttpResponse lastResponse = theInterceptor.getLastResponse(); @@ -505,7 +571,7 @@ public class BaseController { // ContentType ct = lastResponse != null ? ContentType.get(lastResponse.getEntity()) : null; // String mimeType = ct != null ? ct.getMimeType() : null; - + IHttpRequest lastRequest = theInterceptor.getLastRequest(); IHttpResponse lastResponse = theInterceptor.getLastResponse(); String requestBody = null; @@ -524,7 +590,7 @@ public class BaseController { resultStatus = "HTTP " + lastResponse.getStatus() + " " + lastResponse.getStatusInfo(); lastResponse.bufferEntity(); resultBody = IOUtils.toString(lastResponse.readEntity(), Constants.CHARSET_UTF8); - + List ctStrings = lastResponse.getHeaders(Constants.HEADER_CONTENT_TYPE); if (ctStrings != null && ctStrings.isEmpty() == false) { ct = ContentType.parse(ctStrings.get(0)); @@ -543,25 +609,25 @@ public class BaseController { resultDescription.append("Non-FHIR response"); } else { switch (ctEnum) { - case JSON: - if (theResultType == ResultType.RESOURCE) { - narrativeString = parseNarrative(theRequest, ctEnum, resultBody); - resultDescription.append("JSON resource"); - } else if (theResultType == ResultType.BUNDLE) { - resultDescription.append("JSON bundle"); - riBundle = context.newJsonParser().parseResource(resultBody); - } - break; - case XML: - default: - if (theResultType == ResultType.RESOURCE) { - narrativeString = parseNarrative(theRequest, ctEnum, resultBody); - resultDescription.append("XML resource"); - } else if (theResultType == ResultType.BUNDLE) { - resultDescription.append("XML bundle"); - riBundle = context.newXmlParser().parseResource(resultBody); - } - break; + case JSON: + if (theResultType == ResultType.RESOURCE) { + narrativeString = parseNarrative(theRequest, ctEnum, resultBody); + resultDescription.append("JSON resource"); + } else if (theResultType == ResultType.BUNDLE) { + resultDescription.append("JSON bundle"); + riBundle = context.newJsonParser().parseResource(resultBody); + } + break; + case XML: + default: + if (theResultType == ResultType.RESOURCE) { + narrativeString = parseNarrative(theRequest, ctEnum, resultBody); + resultDescription.append("XML resource"); + } else if (theResultType == ResultType.BUNDLE) { + resultDescription.append("XML bundle"); + riBundle = context.newXmlParser().parseResource(resultBody); + } + break; } } @@ -599,6 +665,22 @@ public class BaseController { } + /** + * A hook to be overridden by subclasses. The overriding method can modify the error message + * based on its content and/or the related exception. + * + * @param theErrorMsg The original error message to be displayed to the user. + * @param theException The exception that occurred. May be null. + * @return The modified error message to be displayed to the user. + */ + protected String toDisplayError(String theErrorMsg, Exception theException) { + return theErrorMsg; + } + + protected enum ResultType { + BUNDLE, NONE, RESOURCE, TAGLIST + } + public static class CaptureInterceptor implements IClientInterceptor { private IHttpRequest myLastRequest; @@ -620,7 +702,7 @@ public class BaseController { @Override public void interceptRequest(IHttpRequest theRequest) { assert myLastRequest == null; - + myLastRequest = theRequest; } @@ -667,20 +749,4 @@ public class BaseController { } - protected enum ResultType { - BUNDLE, NONE, RESOURCE, TAGLIST - } - - /** - * A hook to be overridden by subclasses. The overriding method can modify the error message - * based on its content and/or the related exception. - * - * @param theErrorMsg The original error message to be displayed to the user. - * @param theException The exception that occurred. May be null. - * @return The modified error message to be displayed to the user. - */ - protected String toDisplayError(String theErrorMsg, Exception theException) { - return theErrorMsg; - } - }