package com.openMap1.mapper.fhir.server;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import com.openMap1.mapper.core.MapperException;
import com.openMap1.mapper.mapping.MDLBase;
import com.openMap1.mapper.util.GenUtil;
import com.openMap1.mapper.util.ModelUtil;
/**
* class to convert a FHIR search of resource into a Mapper query language query
* @author Robert
*
*/
public class QueryConverter {
// the FHIR servlet that received the query
private FHIRServlet servlet;
// the FHIR class model for the resource
private EPackage classModel;
// the server
private String serverName;
// the resource
private String resourceName;
// parameters of the FHIR search, got from the url
private Map<String,String[]> params;
/* searches supported for this resource, provided the database has the required mappings.
* One search row has columns: server,resource,LHS,type,conditions */
private Vector<String[]> searches;
// comparisons supported in FHIR queries
private int EQUAL = 0;
private int BEFORE = 1; //date before or number less than
private int AFTER = 2; // date after or number greater than
private int BEFORE_OR_EQUAL = 3;
private int AFTER_OR_EQUAL = 4;
// string values in the searches csv file saying which searches are supported for a resource
private String[] fhirTests = {"equal","<",">","<=",">="};
public QueryConverter(FHIRServlet servlet, Map<String,String[]> params,String serverName, String resourceName) throws MapperException
{
this.servlet = servlet;
this.params = params;
this.serverName = serverName;
this.resourceName = resourceName;
searches = servlet.getSearches(serverName, resourceName);
classModel = servlet.getMappedStructure(serverName, resourceName).getClassModelRoot();
writeParams();
}
/**
*
* @return the object query needed to do the search
* @throws MapperException
*/
public Vector<String> queryStrings() throws MapperException
{
// query just returns FHIR ids for eligible resources
String query = "Select " + resourceName + ".fhir_id where ";
// to store chained queries, and to track the variables to be replaced by fhir ids from chained queries in the main query
Vector<String> chainedQueries = new Vector<String>();
// loop over conditions in the search
for (Iterator<String> it = params.keySet().iterator(); it.hasNext();)
{
// if there is a chained query, it will return FHIR ids for referenced resources
String chainedQuery = null;
// LHS of FHIR search condition
String fullParam = it.next();
// possible LHS include 'family:exact' and 'subject:Patient' and 'subject:Patient.name'. The last two are reference searches.
String refType = null; // the name of the referenced resource
String refSearch = null; // for chained searches, the search LHS
StringTokenizer paramParts = new StringTokenizer(fullParam,":");
String param = paramParts.nextToken(); // the part of the LHS before any ':'
if (paramParts.hasMoreTokens())
{
String type = paramParts.nextToken(); // the part of the LHS after the ':'
// exact string search - keep full LHS with ':exact'
if (type.equals("exact")) param = fullParam;
/* typed reference search such as 'subject:Patient=23' or 'subject:Patient.name=peter' ;
* use short LHS and remember the type. Chained reference searches are only supported when the reference type is defined explicitly */
else
{
StringTokenizer refParts = new StringTokenizer(type, ".");
if (refParts.countTokens() > 2) throw new MapperException("Cannot yet handle double-chained searches");
refType = refParts.nextToken(); // type of resource which is referenced, e.g. 'Patient'
if (refParts.hasMoreTokens()) // chained query
{
refSearch = refParts.nextToken(); // search type on the referenced resource, e.g 'name'
chainedQuery = "Select " + refType + ".fhir_id where ";
}
}
}
// RHS of FHIR search condition
String[] values = params.get(fullParam);
if (values.length > 1) throw new MapperException("Multi-value conditions are not yet handled");
String value = values[0];
// check that the LHS is a supported search, and find the paths it requires
String[] pathTypes = getSearchPathsAndType(param,searches);
// add one or two conditions to the query on the main resource, except if they belong in a chained query
if (chainedQuery == null) query = addConditionsToQuery(query, resourceName, param, refType, pathTypes, value);
// when this condition is a chained query
else if (chainedQuery != null)
{
String toReplace = "%" + chainedQueries.size(); // %0, %1, etc. for different chained queries on referenced resources
// add a condition on the main query, whose RHS '%n' will be replaced by an id of the referenced resource
query = addConditionsToQuery(query,resourceName, param, refType, pathTypes, toReplace);
// find supported searches on the referenced resource
Vector<String[]> refSearches = servlet.getSearches(serverName, refType);
// find the paths required, and other parameters of the search on the referenced resource
String[] refPathTypes = getSearchPathsAndType(refSearch,refSearches);
// add one or two conditions to the query on the referenced resource
chainedQuery = addConditionsToQuery(chainedQuery, refType, refSearch, null, refPathTypes, value);
// remove the last " AND " from the chained query
chainedQuery = chainedQuery.substring(0, chainedQuery.length() - 5);
message("Chained object query: " + chainedQuery);
// save the chained query
chainedQueries.add(chainedQuery);
}
}
// remove the last " AND " from the main query
query = query.substring(0, query.length() - 5);
message("Main resource object query: " + query);
// if there were any chained queries, they come before the main query
chainedQueries.add(query);
return chainedQueries;
}
/**
*
* @param query the query to which one or more conditions are to be added
* @param resourceName the resource being queried
* @param param query LHS which defines paths in the resource
* @param refType for reference search conditions, the type of the referenced resource
* @param pathTypes array giving [paths, search type, allowed conditions, solo]
* @param value RHS of the condition (or of two conditions, if it contains '|')
* @throws MapperException
*/
private String addConditionsToQuery(String query, String resourceName, String param, String refType, String[] pathTypes, String value) throws MapperException
{
String newQuery = query;
// unpack the array parameter
String paths = pathTypes[0];
String searchType = pathTypes[1];
String fhirConditionString = pathTypes[2];
// token searches can have one or two values, separated by '|'; and only support equality tests on either
StringTokenizer ss = new StringTokenizer(value,"|");
int valuesToTest = ss.countTokens(); // 1, or may be 2 for a token search . Then the 2 values are [system uri,value]
// look at the beginning of the value to find out what the test is
int FHIRTest = -1;
if (value.startsWith(">")) FHIRTest = AFTER;
else if (value.startsWith("<")) FHIRTest = BEFORE;
else FHIRTest = EQUAL;
// check that all paths needed for the condition are mapped; token searches will need two paths if the RHS contains '|'
StringTokenizer st = new StringTokenizer(paths,"; ");
int pathsDefined = st.countTokens(); // 2 for a token search [system, value], 1 for all other searches
if ((pathsDefined > 1) && (!searchType.equals("token"))) throw new MapperException("Only one path for a non-token search");
// iterate over 1 or 2 paths, matching up with RHS values (from the other StringTokenizer ss)
while (st.hasMoreTokens())
{
String path = st.nextToken();
// if there are 2 paths and only one value to test, use the second path (the value path, not the uri path) to test it
if (pathsDefined > valuesToTest) path = st.nextToken(); // stops further iteration
// for reference searches, servers.csv may optionally leave out the last ".reference" in the path
if ((searchType.equals("reference")) && (!path.endsWith(".reference"))) path = path + ".reference";
// check that there is a chain of mappings, leading to a property mapping for this path
checkMappings(resourceName,path,refType);
// compose the condition for the Object query
String rhsValue = ss.nextToken(); // when there are two values to test, these two StringTokenizers (st and ss) go in step
String theTest = fhirTests[FHIRTest];
// for all tests except '=', strip a comparator such as '<' off the start of the RHS to test
if (FHIRTest != EQUAL) rhsValue = rhsValue.substring(theTest.length());
// check that the FHIR search test is supported for the resource
Vector<String> allowedFhirConditions = new Vector<String>();
StringTokenizer sf = new StringTokenizer(fhirConditionString,";");
while (sf.hasMoreTokens()) allowedFhirConditions.add(sf.nextToken());
if (!GenUtil.inVector(theTest, allowedFhirConditions))
throw new MapperException("FHIR search test '" + theTest
+ "' is not supported for resource " + resourceName + ", search on " + param);
String condition = "=";
if (searchType.equals("date"))
{
// date equality is a test of interval overlap, which can be done with 'startsWith' on the date String.
if (FHIRTest == EQUAL) condition = "startsWith";
// date ranges are done by 'before' and 'after' tests on the date string
if (FHIRTest == BEFORE) condition = "before";
if (FHIRTest == AFTER) condition = "after";
}
// non-exact string search: the '=' test means 'contains', and ignores case
if ((searchType.equals("string")) && (!param.endsWith(":exact")) && (FHIRTest == EQUAL)) condition = "contains";
// for reference searches, the '=' test condition is what we want; no change needed
// add the condition to the object query. Paths begin with the resource class name.
newQuery = newQuery + resourceName + "." + path + " " + condition + " '" + rhsValue + "' AND ";
}
return newQuery;
}
/**
*
* @param param
* @param searches
* @return
* @throws MapperException
*/
private String[] getSearchPathsAndType(String param, Vector<String[]> searches) throws MapperException
{
// check that the LHS is a supported search, and find the paths it requires
String paths = null;
String searchType = null;
String fhirConditionString = null;
String solo = null;
for (int i = 0; i < searches.size(); i++)
{
String[] searchRow = searches.get(i);
String lhs = getValue(searchRow,"LHS");
if (param.equals(lhs))
{
searchType = getValue(searchRow,"type");
paths = getValue(searchRow,"paths");
fhirConditionString = getValue(searchRow,"conditions");
solo = getValue(searchRow,"solo");;
}
}
if (paths == null) throw new MapperException("Server " + servlet.serverName() + ", resource " + servlet.resourceName()
+ " does not support search '" + param + "'");
String[] result = new String[4];
result[0] = paths;
result[1] = searchType;
result[2] = fhirConditionString;
result[3] = solo;
return result;
}
/**
*
* @param searchRow
* @param col
* @return the value of a naemd column in a row of the searches table
* @throws MapperException
*/
private String getValue(String[]searchRow, String col) throws MapperException
{
int cc = -1;
for (int c =0 ; c < servlet.searchColHeaders().length;c++)
if (col.equals(servlet.searchColHeaders()[c])) cc = c;
if (cc == -1) throw new MapperException("Searches table has no column '" + col + "'");
return searchRow[cc];
}
/**
* check if the mappings of the database support the search
* @param path the path , at the end of which there should be a property mapping
* @param refType for types reference searches, the type expected (not checked against mappings yet;
* could be checked if the 'reference' property mapping were annotated with the type of resource referred to)
* @throws MapperException
*/
private void checkMappings(String resourceName,String path,String refType) throws MapperException
{
MDLBase mdl = servlet.getReader(servlet.serverName(), servlet.resourceName());
EClass current = getResourceClass(resourceName);
StringTokenizer steps = new StringTokenizer(path,".");
while (steps.hasMoreTokens())
{
String featName = steps.nextToken();
EStructuralFeature feat = current.getEStructuralFeature(featName);
if (feat == null) throw new MapperException("No feature '" + featName + "' in search path " + path + " from resource class " + resourceName);
String currentClassName = ModelUtil.getQualifiedClassName(current);
// all steps except the last step are EReferences - check the association mapping exists
if (steps.hasMoreTokens())
{
EClass next = (EClass)((EReference)feat).getEType();
String nextClassName = ModelUtil.getQualifiedClassName(next);
if (!mdl.representsAssociationRoleLocally(currentClassName, featName, nextClassName))
throw new MapperException("Database has no mapping for link '" + featName + "' in path " + path + " from resource class " + resourceName);
current = next;
}
// the last step is an EAttribute
else
{
if (!mdl.representsPropertyLocally(currentClassName, featName))
throw new MapperException("Database has no mapping for property '" + featName + "' via path " + path + " from resource class " + resourceName);
}
}
}
/**
*
* @param resourceName
* @return
* @throws MapperException
*/
private EClass getResourceClass(String resourceName) throws MapperException
{
EClass resourceClass = ModelUtil.getNamedClass(classModel, "resources." + resourceName);
if (resourceClass == null) throw new MapperException("Cannot find resource class for " + resourceName);
return resourceClass;
}
/**
*
*/
private void writeParams()
{
for (Iterator<String> it = params.keySet().iterator(); it.hasNext();)
{
String param = it.next();
String[] values = params.get(param);
String pVal = param;
for (int i = 0; i < values.length;i++)
pVal = pVal + "\t" + values[i];
message("Parameter and values: " + pVal);
}
}
// ----------------------------------------------------------------------------------------------------
// odds & sods
// ----------------------------------------------------------------------------------------------------
private void message(String s) {servlet.message(s);}
}