package com.openMap1.mapper.health.cda; import java.util.Hashtable; import java.util.StringTokenizer; import java.util.Vector; import com.openMap1.mapper.core.MapperException; import com.openMap1.mapper.util.GenUtil; public class OCLExpression { private boolean tracing; private void trace(String s) {if (tracing) System.out.println(s);} private Vector<String[]> constraintPaths; public Vector<String[]> constraintPaths() {return constraintPaths;} private String className; private String packageName; private String constraintName; private Vector<String> templatedPackages; private String OCLText; public String OCLText() {return OCLText;} private String[] selectors = {"select","exists","one","forAll","implies"}; private String[] connectors = {"or","xor","and","not","implies","true","false","let","in"}; private String[] giveUpWords = {"let"}; private String[] ignorableStarts = {"first","asSequence","oclAsType","oclIsKindOf","size","isEmpty","oclIsUndefined"}; /** * some OCL constraints are defined by methods rather than paths from the root node. * In those cases , the method follows a path from the root node, and this array defines the path. */ private String[][] methodPaths = { {"all","getAllSections","component.structuredBody.component.section"}, {"all","getSections","component.structuredBody.component.section"}, {"all","getSection","ancestor::section"}, {"all","getEntryTargets","entry.clinicalStatement"}, {"all","getEntryRelationshipTargets","entryRelationship.clinicalStatement"}, {"all","getSubstanceAdministrations","entryOrEntryRelationship.substanceAdministration"}, {"all","getSupplies","entryOrEntryRelationship.supply"}, {"all","getObservations","entryOrEntryRelationship.observation"}, {"all","getActs","entryOrEntryRelationship.act"}, {"all","getOrganizers","entryOrEntryRelationship.organizer"}, {"all","getEncounters","entryOrEntryRelationship.encounter"}, {"all","getProcedures","entryOrEntryRelationship.procedure"}, {"MedicationActivity","getParticipantRoles","consumable.manufacturedProduct"} }; /* In the array methodPaths above, the value 'clinicalStatement' stands for any of the following: */ public static String[] CLINICAL_STATEMENT_VALUES = {"observation","regionOfInterest","observationMedia","substanceAdministration", "supply","procedure","encounter","act","organizer"}; //----------------------------------------------------------------------------------------------------------------- // Constructor //----------------------------------------------------------------------------------------------------------------- public OCLExpression(String packageName,String className, Vector<String> templatedPackages, String OCLText, String constraintName, boolean tracing) throws MapperException { this.packageName = packageName; this.className = className; this.templatedPackages = templatedPackages; this.OCLText = GenUtil.replaceLineBreaksBySpaces(OCLText); this.constraintName = constraintName; this.tracing = tracing; if (parseThisOCL()) constraintPaths = getAllConstraints(); } /** * * @return false if for various reasons I don't want to parse the OCL, * or true if none of those conditions apply, and * this OCL constraint mentions any class in a templated package of the model - e.g 'ccd' */ public boolean parseThisOCL() { if (OCLText.contains(" let ")) return false; // these introduce variables and are to complex to handle if (OCLText.contains("getSections(")) return false; // these have very long paths to poke down into section templates return mentionsClassInTemplatedPackage(OCLText); } /** * @return true if the text mentions any class in a templated package of the model - e.g 'ccd' */ private boolean mentionsClassInTemplatedPackage(String text) { boolean mentions = false; for (int i = 0; i < templatedPackages.size(); i++) { String packName = templatedPackages.get(i); if (text.contains(packName + "::")) mentions = true; } return mentions; } /** * to consume strings like 'self.participant->one(partic : cda::Participant2 | partic.oclIsKindOf(ccd::PatientAwareness))' * and other cases dealt with more or less messily */ public Vector<String[]> getAllConstraints() throws MapperException { Vector<String[]> constraints = new Vector<String[]>(); trace("OCL Text: " + OCLText); String pString = parseBrackets(OCLText); // replace the contents of any (..) by ($key) to look up String path = ""; readDeBracketedExpression(path, pString, constraints); return constraints; } /** * * @param pString * @param constraints * @throws MapperException */ private void readDeBracketedExpression(String path, String pString, Vector<String[]> constraints) throws MapperException { trace("Reading debracketed: " + pString); // consume de-bracketed expression like 'self.getSubstanceAdministrations($k1)->exists($k3) or self.getSupplies($k4)->exists($k6)' StringTokenizer s1 = new StringTokenizer(pString,"-> ="); // spaces and OCL separators outside brackets while (s1.hasMoreTokens()) { String phrase = s1.nextToken(); if (phrase.startsWith("self.")) path = getSelfPath(phrase, path,constraints); else if (GenUtil.inArray(phrase, connectors)) {path = "";} // ignore 'or', 'and', 'implies' etc., but reset the path else if (GenUtil.inArray(phrase, giveUpWords)) {return;} // Don't try to parse OCL containing 'let' else if (hasIgnorableStart(phrase)) {} else if (isInteger(phrase)) {}// ignore an integer value from 'size = 1' else if (hasSelectorStart(phrase)) {handleSelector(path, getBracketKey(phrase),constraints);} else if ((phrase.startsWith("($")) && (phrase.endsWith(")"))) // plain brackets with a key in them { String contents = fragments.get(getBracketKey(phrase)); readDeBracketedExpression(path,contents,constraints); } else throw new MapperException("OCL phrase starts with '" + phrase + "'"); } } /** * * @param first * @return */ private boolean hasIgnorableStart(String first) { boolean isIgnorable = false; for (int i = 0; i < ignorableStarts.length;i++) { if (first.startsWith(ignorableStarts[i])) isIgnorable = true; if (first.startsWith("." + ignorableStarts[i])) isIgnorable = true; } return isIgnorable; } /** * * @param first * @return */ private boolean hasSelectorStart(String first) { boolean isSelector = false; for (int i = 0; i < selectors.length;i++) { if (first.startsWith(selectors[i])) isSelector = true; if (first.startsWith("." + selectors[i])) isSelector = true; } return isSelector; } /** * * @param first a string like 'one($k5)' or just '($k5)' * @return the key to the bracket contents, e.g '$k5' */ private String getBracketKey(String first) { String key = ""; StringTokenizer st = new StringTokenizer(first,"()"); key = st.nextToken(); if (st.hasMoreTokens()) key = st.nextToken(); return key; } /** * consume any substring starting with 'self.' and return the path following the 'self.'. * Also, if there is anything interesting in a final bracket, use it to make constraints * @param selfString * @param path * @param constraints */ private String getSelfPath(String selfString, String pathStart, Vector<String[]> constraints) throws MapperException { trace("Handling 'self' string '" + selfString + "'"); StringTokenizer st = new StringTokenizer(selfString,"()"); String path = st.nextToken().substring(5); // remove "self." if (!pathStart.equals("")) path = pathStart + "." + path; if (st.hasMoreTokens()) // path is a method ending in '(..)' which is replaced by ($k5); look up the real path { path = lookupPath(path); String inBracket = fragments.get(st.nextToken()); // may be empty, or may contain a constrained class name // deduce constraints from method arguments if (mentionsClassInTemplatedPackage(inBracket)) addMethodArgumentConstraint(constraints, inBracket,path); } return path; } /** * * @param method * @return the path used by that method to return a node * @throws MapperException */ private String lookupPath(String method) throws MapperException { trace("Looking up path '" + method + "'"); // some things which are not method names for (int i = 0; i < this.ignorableStarts.length; i++) { String ignorable = ignorableStarts[i]; if (method.endsWith(ignorable)) return method.substring(0,method.length() - ignorable.length()); } // normal method name lookup String path = ""; int priority = 0; for (int i = 0; i < methodPaths.length;i++) { String [] trial = methodPaths[i]; // matches specific to the class - apply whenever found if (trial[0].equals(className)) { if (trial[1].equals(method)) { path = trial[2]; priority = 2; } } // general matches - apply only if no class-specific match has been found else if ((trial[0].equals("all")) && (priority == 0)) { if (trial[1].equals(method)) { path = trial[2]; priority = 1; } } } if (priority == 0) throw new MapperException("No path supplied for method '" + method + "' in class " + className + " of package " + packageName); trace("Looked up path '" + path + "'"); return path; } /** * * @param path * @param bracketKey * @param constraints */ private void handleSelector(String path, String bracketKey,Vector<String[]> constraints) throws MapperException { String inBracket = fragments.get(bracketKey); // contents of bracket after selector replaced by a key if (inBracket == null) throw new MapperException("No bracket contents after selector keyword"); if (path.equals("")) trace("Empty path for selector bracket contents '" + inBracket + "'"); else { Vector<String[]> cons = getConstraints(path,inBracket); for (int i = 0; i < cons.size(); i++) constraints.add(cons.get(i)); } } /** * process a part of an OCL expression like 'self.getEntryRelationshipTargets(vocab::x_ActRelationshipEntryRelationship::SUBJ, ccd::ProblemAct)' * where one of the arguments of the method defines a constrained class * @param constraints a Vector of constraints to be added to * @param inBracket the contents of the bracket defining the function arguments * @param path the path deduced from the function name * add one or more String arrays of 4 elements: * element 0 is a path from the owning node * element 1 is the package of the constrained class * element 2 is the constrained subclass * element 4 is the constraint name */ private void addMethodArgumentConstraint(Vector<String[]> constraints, String inBracket,String path) { trace("Adding method argument from '" + inBracket + "'"); StringTokenizer args = new StringTokenizer(inBracket,", "); while (args.hasMoreTokens()) { String arg = args.nextToken(); if (mentionsClassInTemplatedPackage(arg)) { String[] constraint = new String[4]; constraint[0] = path; constraint[1] = templatePackageName(arg); constraint[2] = constrainedClassName(arg); constraint[3] = constraintName; constraints.add(constraint); } } } /** * * @param s * @return true if s is an integer */ private boolean isInteger(String s) { boolean isNumber = false; try {new Integer(s); isNumber= true;} catch (Exception ex) {} return isNumber; } /** * @param path the path to get to a variable * @param inBracket a String of a form like: 'partic : cda::Participant2 | partic.oclIs(ccd::PatientAwareness)', * got from inside a bracket and containing 'oclIs(ccd::' * @return a Vector of String arrays of 3 elements: * element 0 is a path from the owning node * element 1 is the package of the constrained class * element 2 is the constrained subclass */ private Vector<String[]> getConstraints(String path, String inBracket) throws MapperException { trace("getConstraints with path '" + path + "' and bracket contents '" + inBracket + "'"); // split around the central '|' StringTokenizer split = new StringTokenizer(inBracket,"|"); if (split.countTokens() != 2) throw new MapperException("Bracket contents '" + inBracket + "' do not have one '|'"); String varDeclaration = split.nextToken(); // variable declaration before the '|' String pathConstraint = split.nextToken(); // the rest after the '|' // parse the variable declaration; keep only part before ':' as in 'partic : cda::Participant2' or just 'act ' StringTokenizer decl = new StringTokenizer(varDeclaration,": "); String varName = decl.nextToken(); // parse the remaining path constraints (there may be several separated by 'and' or 'or' with spaces and brackets) Vector<String[]> constraints = parsePathConstraints( pathConstraint, path, varName); return constraints; } /** * * @param pathConstraint a String like 'partic.oclIs(ccd::PatientAwareness)' , but which may contain extra brackets * @param path to be prepended on any path in this text section * @param varName e.g 'partic', to be replaced by the path * * @return a Vector of String arrays of 4 elements: * element 0 is a path from the owning node * element 1 is the package of the constrained class * element 2 is the constrained subclass * element 3 is the constraint name */ private Vector<String[]> parsePathConstraints(String pathConstraint,String path,String varName) throws MapperException { trace("Parsing path constraint '" + pathConstraint + "'"); Vector<String[]> constraints = new Vector<String[]>(); StringTokenizer rem = new StringTokenizer(pathConstraint," "); while (rem.hasMoreTokens()) { String section = rem.nextToken(); // looking for one section like 'partic.oclIsKindOf(ccd::PatientAwareness)' if (section.contains("oclIsKindOf")) { String[] constraint = getKindOfConstraint(path, varName, section); if (constraint != null) constraints.add(constraint); } else if (section.startsWith("($")) // section is like '($k5)' { String key = section.substring(1,section.length()-1); String inBracket = fragments.get(key); if (inBracket != null) { Vector<String[]> cons = parsePathConstraints(inBracket, path, varName); for (int i = 0; i < cons.size(); i++) constraints.add(cons.get(i)); } else throw new MapperException("Cannot resolve bracket '" + section + "'"); } } return constraints; } /** * * @param path to be prepended on any path in this text section * @param varName e.g 'partic', to be replaced by the path * @param section e.g 'partic.oclIsKindOf(ccd::PatientAwareness)' * @param fragments * @return a string array of dimension 4: * element [0] the path * element [1] package name of the constrained class, e.g. 'ccd' * element [2] class name of the constrained class, e.g. 'PatientAwareness' * element [3] is the constraint name */ private String[] getKindOfConstraint(String path,String varName,String section) { trace("Kind of constraint: " + section); String[] constraint = new String[4]; String fullPath = path + "."; boolean foundKeyword = false; StringTokenizer st = new StringTokenizer(section,"()."); while (st.hasMoreTokens()) { String token = st.nextToken(); if (token.equals(varName)) {} // the initial variable name is replaced by the path to the variable else if (token.equals("oclIsKindOf")) {foundKeyword = true;} else if (!foundKeyword) fullPath = fullPath + token + "."; // extend the path else if (token.startsWith("$")) // find package name and class name inside the bracket, having got bracket contents from the key { String fullClassName = fragments.get(token); constraint[1] = templatePackageName(fullClassName); constraint[2] = constrainedClassName(fullClassName); constraint[3] = constraintName; } } // path, with final '.' removed if (foundKeyword) constraint[0] = fullPath.substring(0,fullPath.length()-1); if ((constraint[0] == null)|(constraint[1] == null)) return null; return constraint; } /** * * @param fullClassName e.g 'ccd::PatientAwareness' * @return the package name, e.g. 'ccd', as long as it is a package with template constraints; * if not return null */ private String templatePackageName(String fullClassName) { StringTokenizer st = new StringTokenizer(fullClassName," :"); // allow for random spaces too String pName = st.nextToken(); if (!GenUtil.inVector(pName, templatedPackages)) pName = null; return pName; } /** * * @param fullClassName e.g 'ccd::PatientAwareness' * @return the class name, e.g 'PatientAwareness' */ private String constrainedClassName(String fullClassName) { StringTokenizer st = new StringTokenizer(fullClassName," :"); // allow for random spaces too if (st.countTokens() != 2) return null; st.nextToken(); return st.nextToken(); } /** * * @return a series of [path,package,class, constraint name] constraints */ public String showConstraintPaths() { String paths = ""; for (int i = 0; i < constraintPaths.size();i++) { String[] constraint = constraintPaths.get(i); paths = paths + "[" + constraint[0] + "," + constraint[1] + "," + constraint[2] + "," + constraint[3] + "] "; } return paths; } //-------------------------------------------------------------------------------------------------- // Separating out the contents of brackets //-------------------------------------------------------------------------------------------------- private Hashtable<String,String> fragments = new Hashtable<String,String>(); private int keyIndex = 1; /** * parse the bracket structure of a String to any level up to 20, * denoting the contents of any bracket by a key starting with '$' * @param input * @return the top level string, with ($k2) for the contensts of top-level brackets * @throws MapperException if brackets do not balance */ private String parseBrackets(String input) throws MapperException { String[] stack = new String[20]; //maximum depth of nesting of brackets stack[0] = ""; int level = 0; StringTokenizer st = new StringTokenizer(input,"()",true); while (st.hasMoreTokens()) { String step = st.nextToken(); // start a new level of nesting if (step.equals("(")) { level++; stack[level] = ""; } // close off a level, and remember it by a key else if (step.equals(")")) { String key = storeFragment(stack[level]); level--; stack[level] = stack[level] + "(" + key + ")"; } // build up the current level else { stack[level] = stack[level] + step; } } if (level != 0) throw new MapperException("Brackets unbalanced by " + level + " in OCL expression '" + input + "'"); return stack[0]; } /** * store a fragment of text under a unique key, and return the key * @param fragment * @return */ private String storeFragment(String fragment) { String key = "$k" + keyIndex; keyIndex++; fragments.put(key, fragment); return key; } }