Commit 75a8781c authored by R.W.Majeed's avatar R.W.Majeed

prototype implementation of i2b2 server emulation

parent 69820975
.settings/
.classpath
.project
target/
Dependencies
------------
For development/testing, the server can be started with
`mvn -P jetty jetty:run-war`. This functionality needs i2b2 webclient.
You may need to download the webclient from i2b2.org/software and
install the bundle into your local maven repository manually via
```
mvn install:install-file -Dfile=i2b2webclient-1707c.zip -DgroupId=org.i2b2 -DartifactId=webclient -Dversion=1.7.07c -Dpackaging=zip
```
\ No newline at end of file
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<name>HIStream : i2b2 server emulation</name>
<packaging>war</packaging>
<description>
This project emulates the core components
of an i2b2 server backend. Basic functionality
of PM, CRC, ONT and WORK cells allows the
official i2b2 webclient to connect ot this
emulated server.
</description>
<groupId>de.sekmi.histream</groupId>
<artifactId>i2b2-server</artifactId>
<version>0.8-SNAPSHOT</version>
<parent>
<groupId>de.sekmi.histream</groupId>
<artifactId>histream</artifactId>
<version>0.8-SNAPSHOT</version>
</parent>
<profiles>
<profile>
<id>jetty</id>
<build>
<plugins>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<!--<version>9.3.10.v20160621</version> -->
<version>9.3.8.v20160314</version>
<configuration>
<useTestScope>true</useTestScope>
<useProvidedScope>true</useProvidedScope>
<scanIntervalSeconds>5</scanIntervalSeconds>
<contextXml>${project.basedir}/src/main/webapp/WEB-INF/jetty-context.xml</contextXml>
<webApp>
<descriptor>${project.basedir}/src/main/webapp/WEB-INF/jetty-defaults-web.xml</descriptor>
<jettyEnvXml>${project.basedir}/src/main/webapp/WEB-INF/jetty-env.xml</jettyEnvXml>
</webApp>
</configuration>
<dependencies>
<!-- jackson json media filters -->
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.14</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>unpack</id>
<phase>generate-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<!-- this is a non-standard dependency for executing jetty:run-war.
See README.md for instructions. -->
<artifactItem>
<groupId>org.i2b2</groupId>
<artifactId>webclient</artifactId>
<version>1.7.07c</version>
<type>zip</type>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<!-- jetty:run dependencies (cannot be moved to plugin/dependencies because
of errors) -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>2.14</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers.glassfish</groupId>
<artifactId>jersey-gf-cdi</artifactId>
<version>2.14</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers.glassfish</groupId>
<artifactId>jersey-gf-cdi-ban-custom-hk2-binding</artifactId>
<version>2.14</version>
<scope>provided</scope>
</dependency>
<!-- CDI integration -->
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet-core</artifactId>
<version>2.2.5.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
</profiles>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>target/generated-resources</directory>
</resource>
</resources>
<plugins>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>
<!-- template processing -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.23</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package de.sekmi.histream.i2b2.api.crc;
import java.util.List;
public interface QueryInstance {
String getId(); // instance id
QueryStatus getStatus();
List<QueryResult> getResults();
}
package de.sekmi.histream.i2b2.api.crc;
import java.util.List;
import org.w3c.dom.Element;
public interface QueryManager {
QueryMaster runQuery(Element definition, List<ResultType> results);
QueryMaster getQuery(String queryId);
/**
* List queries for user
* @param userId user id
* @return queries
*/
Iterable<QueryMaster> listQueries(String userId);
/**
* Return supported result types
* @return result types
*/
Iterable<ResultType> getResultTypes();
}
package de.sekmi.histream.i2b2.api.crc;
import java.time.Instant;
import org.w3c.dom.Element;
public interface QueryMaster {
String getId();
String getDisplayName();
String getUser();
Element getDefinition();
Instant getCreateDate();
/**
* Get the instance/execution for the query.
* XXX maybe add support for multiple executions later
* @return execution instance
*/
QueryInstance getInstance();
}
package de.sekmi.histream.i2b2.api.crc;
import java.time.Instant;
public interface QueryResult {
String getId(); // result id
String getDescription();
ResultType getResultType();
Integer getSetSize();
Instant getStartDate();
Instant getEndDate();
QueryStatus getStatus();
}
package de.sekmi.histream.i2b2.api.crc;
public enum QueryStatus {
ERROR, COMPLETED(6), FINISHED(3), INCOMPLETE, WAITTOPROCESS, PROCESSING;
int typeId;
private QueryStatus(int typeId){
this.typeId = typeId;
}
private QueryStatus(){
this.typeId = 0;
}
}
package de.sekmi.histream.i2b2.api.crc;
public interface ResultType {
Integer getId();
String getName();
String getDescription();
}
package de.sekmi.histream.i2b2.api.ont;
public interface Concept {
String getKey();
String getDisplayName();
default Integer getTotalNum(){return null;}
default Concept getSynonymTarget(){return null;}
boolean hasNarrower();
Iterable<Concept> getNarrower();
boolean hasModifiers();
Iterable<Modifier> getModifiers();
/* TODO implement <facttablecolumn>concept_cd</facttablecolumn>
<tablename>concept_dimension</tablename>
<columnname>concept_path</columnname>
<columndatatype>T</columndatatype>
<operator>LIKE</operator>
<dimcode>\i2b2\Diagnoses\Conditions in the perinatal period (760-779)\</dimcode>
<tooltip> */
}
package de.sekmi.histream.i2b2.api.ont;
public interface Modifier {
}
package de.sekmi.histream.i2b2.api.ont;
public interface Ontology {
Iterable<Concept> getCategories();
Concept getConceptByKey(String key);
}
/**
* This package and its sub packages contain interfaces
* to implement backend functionality for the i2b2 server
* emulation.
* <p>
* </P>
*/
package de.sekmi.histream.i2b2.api;
package de.sekmi.histream.i2b2.api.pm;
public interface Project {
/**
* Unique id
* @return id
*/
String getId();
/**
* Short human readable name for the project.
* @return name
*/
String getName();
void addUserRole(User user, String role);
Iterable<String> getUserRoles(User user);
// TODO removeUser (removes all roles), removeUserRole
}
package de.sekmi.histream.i2b2.api.pm;
/**
* User authorisation and association of projects
*
* @author R.W.Majeed
*
*/
public interface ProjectManager {
User getUserById(String userId);
Project getProjectById(String projectId);
}
package de.sekmi.histream.i2b2.api.pm;
public interface SessionManager {
public interface Session{
String getId();
String getUserId();
String getProjectId();
// XXX set project id? or set at creation?
long getCreationTime();
long getLastAccess();
}
/**
* Access an existing session by its session id.
* This operation will also update the {@link Session#getLastAccess()}
* time stamp.
*
* @param sessionId session id
* @return session or {@code null} if not available or expired.
*/
public Session accessSession(String sessionId);
public Session createSession(String userId);
/**
* Iterate over all sessions. The remove() operation should
* be supported by the iterators.
* @return all sessions
*/
Iterable<Session> allSessions();
}
package de.sekmi.histream.i2b2.api.pm;
import java.security.Principal;
public interface User extends Principal{
String getFullName();
boolean isAdmin();
Iterable<Project> getProjects();
// check password
boolean hasPassword(char[] password);
}
package de.sekmi.histream.i2b2.services;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public abstract class AbstractService {
private static final Logger log = Logger.getLogger(AbstractService.class.getName());
public static final String HIVE_NS="http://www.i2b2.org/xsd/hive/msg/1.1/";
/**
* Service name (for communication to client)
* @return service name, e.g. Workplace Cell
*/
public abstract String getName();
/**
* Service version (for communication to client)
* @return service version, e.g. 1.700
*/
public abstract String getVersion();
DocumentBuilder newDocumentBuilder() throws ParserConfigurationException{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// use schema?
//factory.setSchema(schema);
factory.setNamespaceAware(true);
return factory.newDocumentBuilder();
}
Document parseRequest(DocumentBuilder builder, InputStream requestBody) throws SAXException, IOException{
Document dom = builder.parse(requestBody);
// remove whitespace nodes from message header
Element root = dom.getDocumentElement();
try {
stripWhitespace(root);
} catch (XPathExpressionException e) {
log.log(Level.WARNING, "Unable to strip whitespace from request", e);
}
return dom;
}
private void stripWhitespace(Element node) throws XPathExpressionException{
XPathFactory xf = XPathFactory.newInstance();
// XPath to find empty text nodes.
XPathExpression xe = xf.newXPath().compile("//text()[normalize-space(.) = '']");
NodeList nl = (NodeList)xe.evaluate(node, XPathConstants.NODESET);
// Remove each empty text node from document.
for (int i = 0; i < nl.getLength(); i++) {
Node empty = nl.item(i);
empty.getParentNode().removeChild(empty);
}
}
private void appendTextNode(Element el, String name, String value){
Element sub = (Element)el.appendChild(el.getOwnerDocument().createElement(name));
if( value != null ){
sub.appendChild(el.getOwnerDocument().createTextNode(value));
}
}
Document createResponse(DocumentBuilder builder, Element request_header){
Document dom = builder.newDocument();
Element re = (Element)dom.appendChild(dom.createElementNS(HIVE_NS, "response"));
appendTextNode(re, "i2b2_version_compatible", "1.1");
appendTextNode(re, "hl7_version_compatible", "2.4");
// find sending application from request
NodeList nl = request_header.getElementsByTagName("sending_application");
Element el = (Element)re.appendChild(dom.createElement("receiving_application"));
appendTextNode(el, "application_name", nl.item(0).getFirstChild().getTextContent());
appendTextNode(el, "application_version", nl.item(0).getLastChild().getTextContent());
// TODO read response_header.xml template, fill and parse
return dom;
}
}
package de.sekmi.histream.i2b2.services;
import java.util.logging.Logger;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
@Path("/i2b2/services/OntologyService")
public class OntologyService {
private static final Logger log = Logger.getLogger(OntologyService.class.getName());
@POST
@Path("getSchemes")
public Response getSchemes(){
log.info("schemes");
return Response.ok(getClass().getResourceAsStream("/templates/ont/getSchemes.xml")).build();
}
@POST
@Path("getCategories")
public Response getCategories(){
log.info("categories");
return Response.ok(getClass().getResourceAsStream("/templates/ont/getCategories.xml")).build();
}
@POST
@Path("getTermInfo")
public Response getTermInfo(){
log.info("termInfo");
return Response.ok(getClass().getResourceAsStream("/templates/ont/terminfo.xml")).build();
}
}
package de.sekmi.histream.i2b2.services;
import java.io.IOException;
import java.io.StringWriter;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import freemarker.template.Template;
import freemarker.template.TemplateException;
@Singleton
@Path("/i2b2/services/PMService")
public class PMService {
private static final Logger log = Logger.getLogger(PMService.class.getName());
@Inject
Settings config;
@POST
@Path("getServices")
public Response getServices(){
StringWriter w = new StringWriter(2048);
Map<String, String> map = new HashMap<>();
map.put("timestamp", Instant.now().toString());
try {
Template t = config.getFreemarkerConfiguration().getTemplate("getServices.xml");
t.process(map, w);
} catch (IOException | TemplateException e) {
log.log(Level.SEVERE, "Template error", e);
}
return Response.ok(w.toString()).build();
}
@GET
@Path("test")
public Response test(){
StringWriter w = new StringWriter(2048);
Map<String, String> map = new HashMap<>();
map.put("timestamp", Instant.now().toString());
try {
Template t = config.getFreemarkerConfiguration().getTemplate("getServices.xml");
t.process(map, w);
} catch (IOException | TemplateException e) {
log.log(Level.SEVERE, "Template error", e);
}
return Response.ok(w.toString()).build();
}
}
package de.sekmi.histream.i2b2.services;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
@Path("/i2b2/services/QueryToolService")
public class QueryToolService {
private static final Logger log = Logger.getLogger(QueryToolService.class.getName());
@POST
@Path("request")
public Response request(InputStream body){
Document dom = null;
try {
DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
fac.setIgnoringElementContentWhitespace(true);
fac.setNamespaceAware(true);
DocumentBuilder b = fac.newDocumentBuilder();
dom = b.parse(body);
dom.normalizeDocument();
body.close();
} catch (ParserConfigurationException | SAXException | IOException e) {
log.log(Level.SEVERE,"XML error",e);
return Response.status(500).build();
}
// get request type
NodeList nl = dom.getElementsByTagName("request_type");
String type = null;
if( nl.getLength() != 0 ){
type = nl.item(0).getTextContent();
}
// find request body
Node sib = nl.item(0).getParentNode().getNextSibling();
while( sib != null && sib.getNodeType() == Node.TEXT_NODE && sib.getTextContent().trim().length() == 0 ){
sib = sib.getNextSibling();
}
Element req = null;
if( sib != null && sib.getNodeType() == Node.ELEMENT_NODE ){
req = (Element)sib;
}
return request(type, req);
}
private Response request(String type, Element request){
String rtype = null;
if( request != null ){
rtype = request.getAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "type");
}
log.info("Request:"+type+" type="+rtype);
if( type.equals("CRC_QRY_getResultType") ){
InputStream xml = getClass().getResourceAsStream("/templates/crc/resulttype.xml");
if( xml == null ){
log.warning("resulttype.xml not found");
}
return Response.ok(xml).build();
}else if( type.equals("CRC_QRY_getQueryMasterList_fromUserId") ){
return Response.ok(getClass().getResourceAsStream("/templates/crc/masterlist.xml")).build();
}else if( type.equals("CRC_QRY_runQueryInstance_fromQueryDefinition") ){
// XXX
return Response.ok(getClass().getResourceAsStream("/templates/crc/master_instance_result.xml")).build();
}else if( type.equals("CRC_QRY_deleteQueryMaster") ){
// XXX