package com.openMap1.mapper.fhir.server; import java.util.Enumeration; import java.util.Hashtable; import java.util.Iterator; import java.util.StringTokenizer; import java.util.Vector; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EPackage; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.resource.Resource; import org.hl7.fhir.instance.model.Narrative; import org.hl7.fhir.instance.model.Narrative.NarrativeStatus; import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import com.openMap1.mapper.core.MapperException; import com.openMap1.mapper.query.QueryExecutor; import com.openMap1.mapper.query.QueryParser; import com.openMap1.mapper.query.QueryParserImpl_Ecore; import com.openMap1.mapper.query.QueryStrategy; import com.openMap1.mapper.query.QueryStrategyImpl; import com.openMap1.mapper.query.RDBReader; import com.openMap1.mapper.query.SQLQuery; import com.openMap1.mapper.reader.EMFInstanceFactory; import com.openMap1.mapper.reader.GenericEMFInstanceFactoryImpl; import com.openMap1.mapper.reader.MDLXOReader; import com.openMap1.mapper.reader.objectToken; import com.openMap1.mapper.structures.DBStructure; import com.openMap1.mapper.util.GenUtil; import com.openMap1.mapper.util.ModelUtil; import com.openMap1.mapper.util.XMLUtil; public class FHIRSearchManager { private FHIRServlet servlet; private String serverName; private String serverType; // key = resource id; value = narrative for the resource private Hashtable<String,Narrative> allNarratives; private boolean tracing = false; /** * * @param servlet * @throws MapperException */ public FHIRSearchManager(FHIRServlet servlet,String serverName, String serverType) throws MapperException { this.servlet = servlet; this.serverName = serverName; this.serverType = serverType; } private EPackage getClassModel(String resourceName) throws MapperException { EPackage classModel = servlet.getMappedStructure(serverName, resourceName).getClassModelRoot(); if (classModel == null) throw new MapperException("Cannot get class model for resource " + resourceName); return classModel; } /** * * @param resourceName * @param query * @param ids * @throws MapperException */ public void buildFHIRIds(String resourceName, String query, String[] substitutes, Hashtable<String,String> ids) throws MapperException { // make a parser for the object query // message("parsing object query"); Vector<String[]> errors = new Vector<String[]>(); QueryParser queryParser = new QueryParserImpl_Ecore(getClassModel(resourceName),"X",errors,tracing); // possibly replace quoted strings '%0', '%1' in the query text by fhir ids of refrenced resources String newQuery = query; if (substitutes != null) { newQuery = replaceAllInQuery(query, substitutes); message("substituted query: " + newQuery); } // parse the query boolean parsable = queryParser.parse(newQuery); if (!parsable) throw new MapperException("Cannot parse object query " + newQuery); // define the query strategy (ordering of the QueryClasses made by the parser) QueryStrategy queryStrategy = new QueryStrategyImpl(queryParser); queryStrategy.defineStrategy(); // define subsets for all QueryClasses (needed to make an SQL query) MDLXOReader reader = servlet.getReader(serverName, resourceName); queryStrategy.setSubsets("X",reader); // set up the DOM to retrieve FHIR ids for all resources matching the search setReaderDOMForFHIRIds(reader,queryParser); // execute the query against the DOM QueryExecutor executor = new QueryExecutor(reader,queryParser,queryStrategy); executor.initialiseQuery(); executor.calculateResult(true); // merge duplicate fhir ids,which should not exist executor.setResultVector(); // collect the resource ids Vector<Vector<String[]>> id_results = executor.resultVector(); for (Iterator<Vector<String[]>> it = id_results.iterator();it.hasNext();) { Vector<String[]> resRow = it.next(); String[] firstCell = resRow.get(0); String fhir_id = firstCell[2]; ids.put(fhir_id, "1"); } } /** * replace a series of quoted strings '%0', '%1' etc in an object query * by elements 0, 1, etc of a string array (which are fhir ids of referenced resources) * @param query * @param substitutes * @return */ private String replaceAllInQuery(String query, String[] substitutes) { String newQuery = query; for (int s = 0; s < substitutes.length; s++) { String toReplace = "%" + s; // '%0', '%1', etc String nextQuery = replaceInQuery(newQuery, substitutes[s],toReplace); newQuery = nextQuery; } return newQuery; } /** * replace '<placeholder>' in single quotes by '<substitute>' in single quotes * @param query * @param substitute * @param placeHolder * @return */ private String replaceInQuery(String query, String substitute, String placeHolder) { StringTokenizer st = new StringTokenizer(query,"'",true); String replaced = ""; while (st.hasMoreTokens()) { String piece = st.nextToken(); if (piece.equals(placeHolder)) piece = substitute; replaced = replaced + piece; } return replaced; } /** * * @param query * @return * @throws MapperException */ @SuppressWarnings("unchecked") public EObject getResourceBundle(String resourceName, Hashtable<String,String> ids) throws MapperException { Vector<EObject> resources = new Vector<EObject>(); allNarratives = new Hashtable<String,Narrative>() ; for (Enumeration<String> en = ids.keys();en.hasMoreElements();) { String fhir_id = en.nextElement(); EObject resourceObject = getResource(resourceName,fhir_id); if (resourceObject == null) message("Failed to retrieve resource with id " + fhir_id); else { resources.add(resourceObject); Narrative nar = makeNarrative(resourceObject,resourceName); if (nar != null) allNarratives.put(fhir_id, nar); else message("no narrative for resource " + resourceName); } } // create the top AtomFeed EObject, and add the resources to it MDLXOReader reader = servlet.getReader(serverName, resourceName); EObject feedObject = ModelUtil.createModelObject("feed.AtomFeed", reader.classModel()); EClass feedClass = ModelUtil.getNamedClass(reader.classModel(), "feed.AtomFeed"); EStructuralFeature resourceFeature = feedClass.getEStructuralFeature(GenUtil.initialLowerCase(resourceName)); if (resourceFeature == null) throw new MapperException("Cannot find resource feature of AtomFeed object"); Object featureVal = feedObject.eGet(resourceFeature); if (featureVal instanceof EList<?>) for (int i = 0; i < resources.size();i++) ((EList<EObject>)featureVal).add(resources.get(i)); else throw new MapperException("resource feature is not a list"); return feedObject; } /** * * @param reader * @param queryParser * @throws MapperException */ private void setReaderDOMForFHIRIds(MDLXOReader reader,QueryParser queryParser) throws MapperException { // RDBMS server; execute the SQL query and build the DOM from it if (serverType.equals(FHIRServlet.RDBMS)) { // make an RDBReader DBStructure database = servlet.getDBStructure(serverName); RDBReader rdbReader = new RDBReader(database,"noFile"); // convert the object model query into (one) SQL query to populate an XML DOM Vector<SQLQuery> queries = queryParser.makeSQLQueries("X", database); // run the SQL query and convert the result set to an XML DOM rdbReader.initiateQuery(queries); Element rootNode = rdbReader.DOMFromSQL(queries); reader.setRoot(rootNode); } // XML based server; just use the whole DOM else if (serverType.equals(FHIRServlet.XML)) { reader.setRoot(servlet.getDocumentRoot(serverName)); } } /** * * @param resourceName * @param fhir_id * @return * @throws MapperException */ public EObject getResource(String resourceName, String fhir_id) throws MapperException { // object query to return all attributes of the resource and all its component classes String objectQuery = "select " + resourceName + ".** where " + resourceName + ".fhir_id = '" + fhir_id + "'"; message("Query: " + objectQuery); // Parse the object query Vector<String[]> errors = new Vector<String[]>(); QueryParser queryParser = new QueryParserImpl_Ecore(getClassModel(resourceName),"X",errors,tracing); boolean parsable = queryParser.parse(objectQuery); if (!parsable) throw new MapperException("Cannot parse object query " + objectQuery); // define the query strategy (ordering of the QueryClasses made by the parser), in order to define the mapping subsets QueryStrategy queryStrategy = new QueryStrategyImpl(queryParser); queryStrategy.defineStrategy(); MDLXOReader reader = servlet.getReader(serverName, resourceName); queryStrategy.setSubsets("X",reader); Element rootNode = getDOMForOneResource(queryParser); return getResourceFromDOM(reader,rootNode, resourceName, fhir_id); } /** * * @param queryParser * @return * @throws MapperException */ private Element getDOMForOneResource(QueryParser queryParser) throws MapperException { Element rootNode = null; // RDBMS case; execute the SQL query and build the DOM from it if (serverType.equals(FHIRServlet.RDBMS)) { // define the SQL query DBStructure database = servlet.getDBStructure(serverName); Vector<SQLQuery> queries = queryParser.makeSQLQueries("X", database); // run the SQL query and convert the result set to an XML DOM RDBReader rdbReader = new RDBReader(database,"noFile"); rdbReader.initiateQuery(queries); rootNode = rdbReader.DOMFromSQL(queries); } // XML case; just use the whole DOM else if (serverType.equals(FHIRServlet.XML)) { rootNode = servlet.getDocumentRoot(serverName); } return rootNode; } /** * * @param reader * @param rootNode * @param resourceName * @param id used only for error messages * @return the EObject for a resource, as represented by an XML DOM through mappings * @throws MapperException */ private EObject getResourceFromDOM(MDLXOReader reader, Element rootNode, String resourceName, String id) throws MapperException { reader.setRoot(rootNode); EObject resourceObject = null; // get the objectToken for the resource Vector<objectToken> resourceObjects = reader.getAllLocalObjectTokens("resources." + resourceName); // the equality condition on fhir id should mean there is at most one resource object if (resourceObjects.size() > 1) throw new MapperException(resourceObjects.size() + " resources of type " + resourceName + " with id " + id); // allow for cases where no resource objectToken is found - return null if (resourceObjects.size() > 0) { objectToken topObjectToken = resourceObjects.get(0); URI noFile = GenericEMFInstanceFactoryImpl.DO_NOT_SAVE_URI(); EMFInstanceFactory factory = new GenericEMFInstanceFactoryImpl(); // note - res is an EMF Resource, not a FHIR resource Resource res = factory.createModelInstance(reader, noFile, topObjectToken); if (res.getContents().size() > 0) resourceObject = res.getContents().get(0); } else message("No top object found"); return resourceObject; } /** * * @param serverName * @return * @throws MapperException */ public EObject getConformance(String serverName) throws MapperException { Element rootNode = new Conformance(servlet).makeConfigDom(); MDLXOReader conformanceReader = servlet.getReader(serverName, "Conformance"); if (conformanceReader == null) throw new MapperException("Cannot find conformance mappings for server " + serverName); return getResourceFromDOM(conformanceReader,rootNode, "Conformance", "no_id"); } // ---------------------------------------------------------------------------------------------------- // Narratives for Resources // ---------------------------------------------------------------------------------------------------- public Narrative getNarrative(String fhir_id) {return allNarratives.get(fhir_id);} /** * * @param resource * @return a Narrative object in the FHIR reference implementation */ public Narrative makeNarrative(EObject resource, String resourceName) throws MapperException { // read a template DOM and fill in its values from the resource Element tableEl = makeNarrativeDom(resource, resourceName); if (tableEl != null) { // make a generated Narrative Narrative narrative = new Narrative(); narrative.setStatusSimple(NarrativeStatus.generated); // convert the filled template DOM to Xhtml, as in the reference implementation, and add it to the Narrative XhtmlNode node = makeXhtmlNode(tableEl); narrative.setDiv(node); return narrative; } return null; } /** * make an XhtmlNode with all its descendants from a DOM node and its descendants * @param DOMNode * @return */ private XhtmlNode makeXhtmlNode(Element DOMNode) throws MapperException { String nodeName = XMLUtil.getLocalName(DOMNode); if (nodeName == null) throw new MapperException("Null DOM node name"); String text = XMLUtil.getText(DOMNode); Vector<Element> childEls = XMLUtil.childElements(DOMNode); XhtmlNode node = new XhtmlNode(NodeType.Element,nodeName); // XhtmlNode node = new XhtmlNode();node.setNodeType(NodeType.Element);node.setName(nodeName); if ((text != null) && (text.length() > 0)) { XhtmlNode textNode = new XhtmlNode(NodeType.Text); //XhtmlNode textNode = new XhtmlNode();textNode.setNodeType(NodeType.Text); textNode.setContent(text); node.getChildNodes().add(textNode); } NamedNodeMap map = DOMNode.getAttributes(); for (int n = 0; n < map.getLength();n++) { Node nd = map.item(n); if (nd instanceof Attr) { Attr att = (Attr)nd; String attName = att.getName(); String attVal = att.getValue(); node.setAttribute(attName, attVal); } } for (int i = 0; i < childEls.size(); i++) node.getChildNodes().add(makeXhtmlNode(childEls.get(i))); return node; } /** * get a template narrative DOM and fill it out with values from the resource * @param resource * @param resouceName * @return */ private Element makeNarrativeDom(EObject resource, String resourceName) throws MapperException { // get the narrative template DOM for the resource type Element templateEl = servlet.getNarrativeTemplate(serverName, resourceName); if (templateEl != null) { Document doc = XMLUtil.makeOutDoc(); return filledElement(doc, templateEl, resource); } else message("found no template"); return null; } /** * recursive descent of the narrative template, filling it with values from the resource * @param doc * @param templateEl * @param resource * @return * @throws MapperException */ private Element filledElement(Document doc, Element templateEl, EObject resource) throws MapperException { Element filledEl = null; String elName = XMLUtil.getLocalName(templateEl); String content = XMLUtil.getText(templateEl); // substitute the text content of this element if required to if ((content.length() > 2) && (content.startsWith("$"))) { String separator = content.substring(1,2); String paths = content.substring(2); content = getPathsValue(resource,paths,separator); } if (content.length() > 0) filledEl = XMLUtil.textElement(doc, elName, content); else filledEl = XMLUtil.newElement(doc, elName); // copy across attributes, except 'repeat' NamedNodeMap nl = templateEl.getAttributes(); for (int n = 0; n < nl.getLength();n++) { Node nd = nl.item(n); if (nd instanceof Attr) { Attr att = (Attr)nd; if (!att.getName().equals("repeat")) filledEl.setAttribute(att.getName(), att.getValue()); } } // iterate over child nodes Vector<Element> children = XMLUtil.childElements(templateEl); for (int c = 0; c < children.size(); c++) { Element templateChild = children.get(c); String repeat = templateChild.getAttribute("repeat"); // no repeat; stay where you are in the resource EObject if (repeat.length() == 0) filledEl.appendChild(filledElement(doc,templateChild,resource)); // repeat required; find all descendant EObjects else if (repeat.length() > 0) { Vector<EObject> resourceChildren = getRepeats(resource,repeat); for (int r = 0; r < resourceChildren.size(); r++) { EObject resourceChild = resourceChildren.get(r); filledEl.appendChild(filledElement(doc,templateChild, resourceChild)); } } } return filledEl; } /** * * @param resource * @param path * @return * @throws MapperException */ private Vector<EObject> getRepeats(EObject resource,String path) throws MapperException { Vector<EObject> repeats = new Vector<EObject>(); StringTokenizer steps = new StringTokenizer(path, "."); String firstStep = steps.nextToken(); String rest = ""; if (steps.hasMoreTokens()) rest = path.substring(firstStep.length() +1); EStructuralFeature feat = resource.eClass().getEStructuralFeature(firstStep); if (feat == null) throw new MapperException("Feature '" + firstStep + "' in path '" + path + "' not recognised"); Object nextObj = resource.eGet(feat); if (nextObj instanceof EObject) { if (rest.equals("")) repeats.add((EObject)nextObj); else repeats = getRepeats((EObject)nextObj,rest); } else if (nextObj instanceof EList<?>) { EList<?> nextList = (EList<?>)nextObj; for (int i = 0; i < nextList.size();i++) { EObject obj = (EObject)nextList.get(i); if (rest.length() > 0) { Vector<EObject> further = getRepeats(obj,rest); for (int f = 0; f < further.size(); f++) repeats.add(further.get(f)); } else repeats.add(obj); } } return repeats; } /** * follow several paths in an EObject and return the concatenated string values * @param resource * @param paths * @param separator * @return * @throws MapperException */ private String getPathsValue(EObject resource, String paths,String separator) throws MapperException { StringTokenizer st = new StringTokenizer(paths,"+ "); String value = ""; while (st.hasMoreTokens()) value = value + getPathValue(resource, st.nextToken()) + separator; return value.substring(0,value.length() - separator.length()); } /** * follow a path in a EObject and return an attribute value * @param resource * @param path * @return * @throws MapperException */ private String getPathValue(EObject resource, String path) throws MapperException { EObject current = resource; StringTokenizer st = new StringTokenizer(path,"."); while (st.hasMoreTokens()) { String step = st.nextToken(); EClass currentClass = current.eClass(); EStructuralFeature feat = currentClass.getEStructuralFeature(step); if (feat == null) throw new MapperException("Feature '" + step + "' in path '" + path + "' not recognised"); Object nextObj = current.eGet(feat); // associations along the path if (st.hasMoreTokens()) { if (nextObj instanceof EObject) current = (EObject)nextObj; // when an EReference has maxMult > 1, take the first element else if (nextObj instanceof EList<?>) current = (EObject)((EList)nextObj).get(0); } // property at the final step of the path else if (nextObj == null) return "--"; else if (nextObj instanceof String) return (String)nextObj; else throw new MapperException ("Property " + feat + " is not a string"); } return ""; } /** * diagnostic recursive write of xhtml node names * @param node * @param level */ private void writeNode(XhtmlNode node, int level) { message("Level " + level + ": " + node.getName()); for (int i = 0; i < node.getChildNodes().size(); i++) writeNode(node.getChildNodes().get(i), level +1); } // ---------------------------------------------------------------------------------------------------- // odds & sods // ---------------------------------------------------------------------------------------------------- private void message(String s) {servlet.message(s);} }