Java tutorial
/* * Copyright (c) 2015 Christian Chevalley * This file is part of Project Ethercis * * 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. */ //Copyright package com.ethercis.compositionservice; import com.ethercis.compositionservice.handler.CanonicalHandler; import com.ethercis.compositionservice.handler.FlatJsonHandler; import com.ethercis.dao.access.handler.PvCompoHandler; import com.ethercis.dao.access.interfaces.*; import com.ethercis.ehr.building.I_ContentBuilder; import com.ethercis.ehr.json.FlatJsonUtil; import com.ethercis.ehr.keyvalues.EcisFlattener; import com.ethercis.ehr.knowledge.I_CacheKnowledgeService; import com.ethercis.ehr.util.FlatJsonCompositionConverter; import com.ethercis.ehr.util.I_FlatJsonCompositionConverter; import com.ethercis.logonservice.session.I_SessionManager; import com.ethercis.persistence.ServiceDataCluster; import com.ethercis.servicemanager.annotation.*; import com.ethercis.servicemanager.cluster.I_Info; import com.ethercis.servicemanager.cluster.RunTimeSingleton; import com.ethercis.servicemanager.common.I_SessionClientProperties; import com.ethercis.servicemanager.common.MetaBuilder; import com.ethercis.servicemanager.common.def.Constants; import com.ethercis.servicemanager.common.def.MethodName; import com.ethercis.servicemanager.common.def.SysErrorCode; import com.ethercis.servicemanager.common.identification.IdentificationDef; import com.ethercis.servicemanager.exceptions.ServiceManagerException; import com.ethercis.servicemanager.runlevel.I_ServiceRunMode; import com.ethercis.servicemanager.service.ServiceInfo; import com.ethercis.systemservice.I_SystemService; import org.apache.log4j.Logger; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.joda.time.DateTime; import org.openehr.rm.composition.Composition; import java.io.StringReader; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * Replies to queries with the following format: * GET ../composition?uid=... &format=<FLAT|STRUCTURED|RAW|XML> * DELETE ../composition?uid=...[&committerName=....][&committerId=...] * POST ../composition?[templateId=...][&ehrId=...][&format=...][&committerName=...][&committerId=...] (body contains the serialized composition) * PUT ../composition?uid=...[templateId=...][&ehrId=...][&format=...][&committerName=...][&committerId=...] (body contains the serialized composition) * ETHERCIS Project VirtualEhr * Created by Christian Chevalley on 7/3/2015. */ @Service(id = "CompositionService", version = "1.0", system = true) @RunLevelActions(value = { @RunLevelAction(onStartupRunlevel = 9, sequence = 4, action = "LOAD"), @RunLevelAction(onShutdownRunlevel = 9, sequence = 4, action = "STOP") }) public class CompositionService extends ServiceDataCluster implements I_CompositionService, CompositionServiceMBean { final private String ME = "CompositionService"; final private String Version = "1.0"; private Logger log = Logger.getLogger(CompositionService.class); private I_CacheKnowledgeService knowledgeCache; private I_SystemService systemService; private boolean useNamespaceInCompositionId = false; @Override public void doInit(RunTimeSingleton global, ServiceInfo serviceInfo) throws ServiceManagerException { super.doInit(global, serviceInfo); //get a resource service instance //get the knowledge cache for composition handlers useNamespaceInCompositionId = global.getProperty().get("composition.uid.namespace", true); knowledgeCache = getRegisteredService(getGlobal(), "CacheKnowledgeService", "1.0"); if (knowledgeCache == null) throw new ServiceManagerException(global, SysErrorCode.RESOURCE_CONFIGURATION, ME, "Cache knowledge service [CacheKnowledgeService,1.0] is not running, aborting"); putObject(I_Info.JMX_PREFIX + ME, this); log.info("Composition service started..."); } private UUID getSessionEhrId(String sessionId) throws ServiceManagerException { I_SessionManager sessionManager = getRegisteredService(getGlobal(), "LogonService", "1.0"); //retrieve the session manager return (UUID) sessionManager.getSessionUserMap(sessionId).get(EHR_ID); } private UUID retrieveEhrId(String sessionId, I_SessionClientProperties props) throws ServiceManagerException { String uuidEncoded = props.getClientProperty(I_CompositionService.EHR_ID, (String) null); if (uuidEncoded == null) { if (getSessionEhrId(sessionId) != null) uuidEncoded = getSessionEhrId(sessionId).toString(); else throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "No Ehr Id found in context nor in query"); } UUID ehrId = UUID.fromString(uuidEncoded); return ehrId; } @QuerySetting(dialect = { @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.STANDARD, httpMethod = "GET", method = "create", path = "vehr/composition", responseType = ResponseType.Json), @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.EHRSCAPE, httpMethod = "POST", method = "post", path = "rest/v1/composition", responseType = ResponseType.Json) }) public Object create(I_SessionClientProperties props) throws Exception { auditSetter.handleProperties(getDataAccess(), props); String sessionId = auditSetter.getSessionId(); String templateId = props.getClientProperty(I_CompositionService.TEMPLATE_ID, (String) null); UUID ehrId = retrieveEhrId(sessionId, props); UUID committerUuid = auditSetter.getCommitterUuid(); UUID systemUuid = auditSetter.getSystemUuid(); I_CompositionService.CompositionFormat format = I_CompositionService.CompositionFormat .valueOf(props.getClientProperty(I_CompositionService.FORMAT, "XML")); if ((format == CompositionFormat.FLAT || format == CompositionFormat.ECISFLAT) && (templateId == null || templateId.length() == 0)) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Template Id must be specified"); //get body stuff String content = props.getClientProperty(Constants.REQUEST_CONTENT, (String) null); if (content == null) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Content cannot be empty for a new composition"); Integer contentLength = (Integer) props.getClientProperty(Constants.REQUEST_CONTENT_LENGTH, (Integer) 0); if (content.length() != contentLength) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Content may be altered found length =" + content.length() + " expected:" + contentLength); // String contentType = props.getClientProperty(Constants.REQUEST_CONTENT_TYPE, ""); UUID compositionId; switch (format) { case XML: CanonicalHandler canonicalHandler = new CanonicalHandler(knowledgeCache.getKnowledgeCache(), templateId, null); Composition composition = canonicalHandler.build(getGlobal(), content); templateId = composition.getArchetypeDetails().getTemplateId().getValue(); I_CompositionAccess compositionAccess = I_CompositionAccess.getNewInstance(getDataAccess(), composition, DateTime.now(), ehrId); I_EntryAccess entryAccess = I_EntryAccess.getNewInstance(getDataAccess(), templateId, 0, compositionAccess.getId(), composition); compositionAccess.addContent(entryAccess); compositionId = compositionAccess.commit(committerUuid, systemUuid, auditSetter.getDescription()); //create an XML response Document document = DocumentHelper.createDocument(); Element root = document.addElement("compositionCreateRestResponseData"); root.addElement("action").addText("CREATE"); root.addElement("compositionUid").addText(encodeUuid(compositionId, 1)); root.addElement("meta").addElement("href") .addText(Constants.URI_TAG + "?" + encodeURI(null, compositionId, 1, null)); global.getProperty().set(MethodName.RETURN_TYPE_PROPERTY, "" + MethodName.RETURN_XML); return document; case ECISFLAT: PvCompoHandler pvCompoHandler = new PvCompoHandler(this.getDataAccess(), templateId, null); Map<String, String> kvPairs = FlatJsonUtil .inputStream2Map(new StringReader(new String(content.getBytes()))); compositionId = pvCompoHandler.storeComposition(ehrId, kvPairs, auditSetter.getCommitterUuid(), auditSetter.getSystemUuid(), auditSetter.getDescription()); //create json response global.getProperty().set(MethodName.RETURN_TYPE_PROPERTY, "" + MethodName.RETURN_JSON); Map<String, Object> retmap = new HashMap<>(); retmap.put("action", "CREATE"); retmap.put(COMPOSITION_UID, encodeUuid(compositionId, 1)); Map<String, Map<String, String>> metaref = MetaBuilder.add2MetaMap(null, "href", Constants.URI_TAG + "?" + encodeURI(null, compositionId, 1, null)); retmap.putAll(metaref); return retmap; case FLAT: I_FlatJsonCompositionConverter flatJsonCompositionConverter = FlatJsonCompositionConverter .getInstance(getDataAccess().getKnowledgeManager()); Map flatMap = FlatJsonUtil.inputStream2Map(new StringReader(new String(content.getBytes()))); Composition newComposition = flatJsonCompositionConverter.toComposition(templateId, flatMap); I_CompositionAccess access = I_CompositionAccess.getNewInstance(getDataAccess(), newComposition, DateTime.now(), ehrId); I_EntryAccess entry = I_EntryAccess.getNewInstance(getDataAccess(), templateId, 0, access.getId(), newComposition); access.addContent(entry); compositionId = access.commit(committerUuid, systemUuid, auditSetter.getDescription()); //create json response global.getProperty().set(MethodName.RETURN_TYPE_PROPERTY, "" + MethodName.RETURN_JSON); retmap = new HashMap<>(); retmap.put("action", "CREATE"); retmap.put(COMPOSITION_UID, encodeUuid(compositionId, 1)); metaref = MetaBuilder.add2MetaMap(null, "href", Constants.URI_TAG + "?" + encodeURI(null, compositionId, 1, null)); retmap.putAll(metaref); return retmap; default: throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "This format is not supported:" + format); } } @QuerySetting(dialect = { @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.STANDARD, httpMethod = "GET", method = "create", path = "vehr/composition", responseType = ResponseType.String), @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.EHRSCAPE, httpMethod = "GET", method = "get", path = "rest/v1/composition", responseType = ResponseType.String) }) public Object retrieve(I_SessionClientProperties props) throws Exception { I_CompositionService.CompositionFormat format = I_CompositionService.CompositionFormat.valueOf( props.getClientProperty(I_CompositionService.FORMAT, CompositionFormat.ECISFLAT.toString())); Integer version = -1; UUID uid = null; String compositionId = props.getClientProperty(I_CompositionService.UID, (String) null); if (compositionId == null) { throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Null or not supplied composition id"); } try { //TODO: decode with/without namespace depending on configuration if (compositionId.contains("::")) { version = getCompositionVersion(compositionId) - 1; //user pass versions as 1 (current), 2, 3, 4 ... uid = getCompositionUid(compositionId); } else uid = getCompositionUid(compositionId); } catch (Exception e) { throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Invalid composition id:" + compositionId); } Object retObj = null; //retrieve the composition I_CompositionAccess compositionAccess = null; if (version > 0) compositionAccess = I_CompositionAccess.retrieveCompositionVersion(getDataAccess(), uid, version); else { compositionAccess = I_CompositionAccess.retrieveInstance(getDataAccess(), uid); if (compositionAccess == null && I_CompositionAccess.hasPreviousVersion(getDataAccess(), uid)) { //try to identify a previous version //TODO: add life_cycle state to versions and return the first non deleted version id... right now it's always 1 global.getProperty().set(MethodName.RETURN_TYPE_PROPERTY, "" + MethodName.RETURN_NO_CONTENT); //build the relative part of the link to the existing last version Map<String, Object> retMap = new HashMap<>(); retMap.put("Link", Constants.URI_TAG + "?" + encodeURI(null, uid, 2, format)); return retMap; } } if (compositionAccess == null) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Request did not give any result"); for (I_EntryAccess entryAccess : compositionAccess.getContent()) { switch (format) { case XML: global.getProperty().set(MethodName.RETURN_TYPE_PROPERTY, "" + MethodName.RETURN_XML); // Document document = DocumentHelper.createDocument(); // Element root = document.addElement("compositionCreateRestResponseData"); // root.addElement("action").addText("RETRIEVE"); // root.addElement("compositionUid").addText(encodeUuid(entryAccess.getCompositionId(),1)); // root.addElement("meta").addElement("href").addText(Constants.URI_TAG+"?"+encodeURI(null, entryAccess.getCompositionId(), 1, null)); retObj = new String(I_ContentBuilder.exportCanonicalXML(entryAccess.getComposition())); // String responseData = document.asXML(); // String[] fragments = responseData.split("</meta>"); // String encoded = fragments[0]+"</meta>"+retObj+fragments[1]; // return encoded; break; case ECISFLAT: global.getProperty().set(MethodName.RETURN_TYPE_PROPERTY, "" + MethodName.RETURN_JSON); Map<String, Object> retmap = new HashMap<>(); retmap.put("format", CompositionFormat.ECISFLAT.toString()); retmap.put("templateId", entryAccess.getTemplateId()); retmap.put("composition", EcisFlattener.renderFlat(entryAccess.getComposition())); Map<String, Map<String, String>> metaref = MetaBuilder.add2MetaMap(null, "href", Constants.URI_TAG + "?" + encodeURI(null, uid, 1, null)); retmap.putAll(metaref); retObj = retmap; break; case FLAT: I_FlatJsonCompositionConverter flatJsonCompositionConverter = FlatJsonCompositionConverter .getInstance(getDataAccess().getKnowledgeManager()); global.getProperty().set(MethodName.RETURN_TYPE_PROPERTY, "" + MethodName.RETURN_JSON); retmap = new HashMap<>(); retmap.put("format", CompositionFormat.FLAT.toString()); retmap.put("templateId", entryAccess.getTemplateId()); retmap.put("composition", flatJsonCompositionConverter.fromComposition(entryAccess.getTemplateId(), entryAccess.getComposition())); metaref = MetaBuilder.add2MetaMap(null, "href", Constants.URI_TAG + "?" + encodeURI(null, uid, 1, null)); retmap.putAll(metaref); retObj = retmap; break; default: throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Unsupported format:" + format); } } return retObj; } @QuerySetting(dialect = { @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.STANDARD, httpMethod = "GET", method = "update", path = "vehr/composition", responseType = ResponseType.Json), @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.EHRSCAPE, httpMethod = "PUT", method = "put", path = "rest/v1/composition", responseType = ResponseType.Json) }) public Object update(I_SessionClientProperties props) throws Exception { auditSetter.handleProperties(getDataAccess(), props); String sessionId = auditSetter.getSessionId(); String templateId = props.getClientProperty(I_CompositionService.TEMPLATE_ID, (String) null); String uidStr = props.getClientProperty(I_CompositionService.UID, (String) null); if (uidStr == null || uidStr.length() == 0) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "A valid composition id must be supplied"); UUID compositionId = getCompositionUid(uidStr); I_CompositionService.CompositionFormat format = I_CompositionService.CompositionFormat.valueOf( props.getClientProperty(I_CompositionService.FORMAT, CompositionFormat.ECISFLAT.toString())); //get body stuff String content = props.getClientProperty(Constants.REQUEST_CONTENT, (String) null); if (content == null) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Content cannot be empty for updating a composition"); Integer contentLength = (Integer) props.getClientProperty(Constants.REQUEST_CONTENT_LENGTH, (Integer) 0); if (content.length() != contentLength) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Content may be altered found length =" + content.length() + " expected:" + contentLength); // String contentType = props.getClientProperty(Constants.REQUEST_CONTENT_TYPE, ""); Boolean result; switch (format) { case XML: CanonicalHandler canonicalHandler = new CanonicalHandler(knowledgeCache.getKnowledgeCache(), templateId, null); result = canonicalHandler.update(getGlobal(), getDataAccess(), compositionId, content, auditSetter.getCommitterUuid(), auditSetter.getSystemUuid(), auditSetter.getDescription()); break; case ECISFLAT: I_CompositionAccess compositionAccess = I_CompositionAccess.retrieveInstance(getDataAccess(), compositionId); if (compositionAccess == null) throw new ServiceManagerException(getGlobal(), SysErrorCode.RESOURCE_NOT_FOUND, ME, "Could not find composition:" + compositionId); //TODO: template id is not required PvCompoHandler pvCompoHandler = new PvCompoHandler(this.getDataAccess(), compositionAccess, "*", null); //template id is not required Map<String, String> kvPairs; try { kvPairs = FlatJsonUtil.inputStream2Map(new StringReader(new String(content.getBytes()))); } catch (Exception e) { throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Error interpreting JSON in content:" + e); } result = pvCompoHandler.updateComposition(kvPairs, auditSetter.getCommitterUuid(), auditSetter.getSystemUuid(), auditSetter.getDescription()); break; case FLAT: compositionAccess = I_CompositionAccess.retrieveInstance(getDataAccess(), compositionId); if (compositionAccess == null) throw new ServiceManagerException(getGlobal(), SysErrorCode.RESOURCE_NOT_FOUND, ME, "Could not find composition:" + compositionId); //get the template id FlatJsonHandler flatJsonHandler = new FlatJsonHandler(getDataAccess(), compositionAccess, null, null); result = flatJsonHandler.update(global, getDataAccess(), compositionId, new String(content.getBytes()), auditSetter.getCommitterUuid(), auditSetter.getSystemUuid(), auditSetter.getDescription()); break; default: throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "This format is not supported:" + format); } //TODO: set committer if passed if (!result) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Update failed on composition:" + compositionId); Map<String, Object> retmap = new HashMap<>(); retmap.put("action", result ? "UPDATED" : "FAILED"); retmap.put(COMPOSITION_UID, encodeUuid(compositionId, 1)); Map<String, Map<String, String>> metaref = MetaBuilder.add2MetaMap(null, "href", Constants.URI_TAG + "?" + encodeURI(null, compositionId, 1, null)); retmap.putAll(metaref); return retmap; } @QuerySetting(dialect = { @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.STANDARD, httpMethod = "GET", method = "delete", path = "vehr/composition", responseType = ResponseType.Json), @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.EHRSCAPE, httpMethod = "DELETE", method = "delete", path = "rest/v1/composition", responseType = ResponseType.Json) }) public Object delete(I_SessionClientProperties props) throws Exception { String uidStr = props.getClientProperty(I_CompositionService.UID, (String) null); if (uidStr == null || uidStr.length() == 0) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "A valid composition id must be supplied"); UUID compositionId = getCompositionUid(uidStr); auditSetter.handleProperties(getDataAccess(), props); String sessionId = auditSetter.getSessionId(); I_CompositionAccess compositionAccess = I_CompositionAccess.retrieveInstance(getDataAccess(), compositionId); if (compositionAccess == null) throw new ServiceManagerException(getGlobal(), SysErrorCode.RESOURCE_NOT_FOUND, ME, "Could not find composition:" + compositionId); Integer result = compositionAccess.delete(auditSetter.getCommitterUuid(), auditSetter.getSystemUuid(), auditSetter.getDescription()); if (result <= 0) throw new ServiceManagerException(getGlobal(), SysErrorCode.USER_ILLEGALARGUMENT, ME, "Delete failed on composition:" + compositionAccess.getId()); Map<String, Object> retmap = new HashMap<>(); retmap.put("action", result > 0 ? "DELETED" : "FAILED"); retmap.put(COMPOSITION_UID, encodeUuid(compositionId, 1)); Map<String, Map<String, String>> metaref = MetaBuilder.add2MetaMap(null, "href", Constants.URI_TAG + "?" + encodeURI(null, compositionId, 1, null)); retmap.putAll(metaref); return retmap; } private enum QueryMode { SQL, AQL, UNDEF } @QuerySetting(dialect = { @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.STANDARD, httpMethod = "POST", method = "post", path = "vehr/query", responseType = ResponseType.Json), @QuerySyntax(mode = I_ServiceRunMode.DialectSpace.EHRSCAPE, httpMethod = "GET", method = "get", path = "rest/v1/query", responseType = ResponseType.Json) }) public Object query(I_SessionClientProperties props) throws Exception { QueryMode queryMode = QueryMode.UNDEF; String sessionId = props.getClientProperty(I_SessionManager.SECRET_SESSION_ID_INTERNAL, (String) null); String queryString = props.getClientProperty(I_CompositionService.SQL_QUERY, (String) null); if (queryString == null) { queryString = props.getClientProperty(I_CompositionService.AQL_QUERY, (String) null); if (queryString != null) { queryMode = QueryMode.AQL; } else throw new ServiceManagerException(global, SysErrorCode.USER_ILLEGALARGUMENT, "No query parameter supplied"); } else queryMode = QueryMode.SQL; //perform the query Map<String, Object> result; switch (queryMode) { case SQL: result = I_EntryAccess.queryJSON(getDataAccess(), queryString); break; case AQL: result = I_EntryAccess.queryAqlJson(getDataAccess(), queryString); break; default: throw new ServiceManagerException(global, SysErrorCode.USER_ILLEGALARGUMENT, "Unknown query expression, should be 'sql=' or 'aql='"); } if (result.size() == 0) { global.getProperty().set(MethodName.RETURN_TYPE_PROPERTY, "" + MethodName.RETURN_NO_CONTENT); //build the relative part of the link to the existing last version Map<String, Object> retMap = new HashMap<>(); retMap.put("Reason", "Query resultset is empty"); return retMap; } return result; } private String encodeUuid(UUID uuid, int version) { if (useNamespaceInCompositionId) return uuid + "::" + getDataAccess().getServerNodeId() + "::" + version; else return uuid + "::" + version; } private UUID getCompositionUid(String fullcompositionUid) { if (!fullcompositionUid.contains("::")) return UUID.fromString(fullcompositionUid); return UUID.fromString(fullcompositionUid.substring(0, fullcompositionUid.indexOf("::"))); } private int getCompositionVersion(String fullcompositionUid) { if (!fullcompositionUid.contains("::")) return 1; //current version return Integer.valueOf(fullcompositionUid.substring(fullcompositionUid.lastIndexOf("::") + 2)); } private String encodeURI(UUID ehrId, UUID compositionId, int version, I_CompositionService.CompositionFormat format) { StringBuffer encoded = new StringBuffer(); if (compositionId != null) encoded.append(I_CompositionService.UID + "=" + encodeUuid(compositionId, version)); if (ehrId != null) encoded.append("&" + I_CompositionService.EHR_ID + "=" + ehrId); if (format != null) encoded.append("&" + I_CompositionService.FORMAT + "=" + format); return encoded.toString(); } }