/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is OpenEMRConnect. * * The Initial Developer of the Original Code is International Training & * Education Center for Health (I-TECH) <http://www.go2itech.org/> * * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * ***** END LICENSE BLOCK ***** */ package ke.go.moh.oec.lib; import ke.go.moh.oec.IService; import ke.go.moh.oec.PersonRequest; import ke.go.moh.oec.PersonResponse; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.MalformedURLException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.Date; import java.util.Properties; import java.util.logging.FileHandler; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; /** * Mediator between OEC clients and services to forward requests * and return responses. This may include storing and forwarding requests * and/or responses when network connections cannot be made. * <p> * If the user of this class * accepts unsolicited messages (as a server), then it must register * a callback object that is compliant with the IService interface. If the * user of this class is just making client requests, this is not necessary. * * @author JGitahi * @author Jim Grace */ public class Mediator implements IService { /** * Maximum number of times a message may be transmitted through the * network on its way from source to destination. * <p> * For example, the first time a message is sent, the hop count is 1. * If the message is received by another program and forwarded on, * the hop count is 2, and so on. * <p> * In the present design, the hop count should never exceed 4. If it does, * it probably indicates a loop where one system is routing the message * to a second system, and the second system is routing it back to the * first system. * <p> * If the hop count exceeds the maximum, the message is discarded and * an error reported. */ private static final int MAX_HOP_COUNT = 4; /** * The number of protocol message IDs generated since this library * was initialized. The sequence number forms part of the message ID. */ private static int messageSequenceNumber = 0; /** * The queue of messages we have sent, for which we expect a response. */ private static MessagePendingQueue pendingQueue = new MessagePendingQueue(); /* * Allocate objects that we will use for this instance of Mediator. * These are instance variables, so they will be thread safe in the * event that different instances of Mediator are operating concurrently * on different threads. */ /** * Instance to handle HTTP protocol. * Start the instance with a reference to us, so it knows where to * deliver any HTTP messages received. */ private HttpService httpService; /* * Instance to manage the store-and-forward message queue. * Start the instance with a reference to the HTTP Handler, so it * knows how to send any messages on the network. */ private QueueManager queueManager; /** Instance to pack and unpack XML */ private XmlPacker xmlPacker; /** * Reference to our caller's callbackObject that implements * {@link IService#getData(int, java.lang.Object)}. */ static private IService myCallbackObject = null; /** * A copy of the properties from standard file location */ static Properties properties = null; /** Lock the properties file we are using, so multiple instances will use multiple properties files. */ static FileLock pathLock = null; /** The logger level to use, configured from the properties file. */ static Level loggerLevel = null; /** Should we use the logging service? */ static boolean useLoggingService = true; /** Directory where we find our properties and QueueManager embedded database. */ static String runtimeDirectory; /** * Initialize -- set up the runtime directory. */ static { setRuntimeDirectory(); // Do this first! } /** * Constructs an instance of the Mediator. * (Note: there should be only one instance of the mediator. At some * point in the future, this class, and all who call it, should * properly be refactored to follow the Java singleton pattern.) * <p> * If we are to use our own distributed logging service, set up the * LoggingServiceHandler to handle all calls to the standard logger. * <p> * Allocate other library class objects as needed, and start them * as needed. In particular, the HttpManager and QueueManager need * to be started. */ public Mediator() { setLoggerLevel(); LogManager man = LogManager.getLogManager(); Logger rootLogger = man.getLogger(""); if (useLoggingService) { Handler loggingServiceHandler = new LoggingServiceHandler(this); Formatter formatter = new SimpleFormatter(); loggingServiceHandler.setFormatter(formatter); rootLogger.addHandler(loggingServiceHandler); } String loggerFile = getProperty("Logger.File"); if (loggerFile != null && Boolean.parseBoolean(loggerFile)) { try { String logFileName = runtimeDirectory + "openemrconnect%g.log"; Handler loggingFileHandler = new FileHandler(logFileName, 100000, 100); Formatter formatter = new SimpleFormatter(); loggingFileHandler.setFormatter(formatter); loggingFileHandler.setLevel(loggerLevel); rootLogger.addHandler(loggingFileHandler); } catch (IOException ex) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "Can''t start file logger.", ex); } catch (SecurityException ex) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "Can''t start file logger.", ex); } } httpService = new HttpService(this); queueManager = new QueueManager(httpService); xmlPacker = new XmlPacker(); try { httpService.start(); } catch (IOException ex) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, null, ex); } queueManager.start(); Logger.getLogger(Mediator.class.getName()).log(Level.FINE, "{0} started.", getProperty("Instance.Name")); } /** * Sets up the runtimeDirectory string to select a working directory relative * to the default application directory. The purpose is to allow multiple * instances of the code to run from the same directory. This can be * especially useful in debugging and testing environments. The default * directory for each instance is used to contain the openemrconnect.properties * file used by that instance. It may contain other directories or files as * well, such as the embedded JavaDB database used for the QueueManager. * <p> * The runtimeDirectory is determined as follows: The first time an application * is run, it places a lock on a dummy file in the default application directory. * When this lock is successfully in place, it then leaves the runtimeDirectory * set to an empty string -- meaning that the runtime directory is the same * as the default application directory. * <p> * The second time an application is run concurrently from the same * application directory, we will find that the dummy file is already * locked by the first instance of the application. In that event, we * will try a subdirectory of "runtime2/", relative to the default * application directory. */ static void setRuntimeDirectory() { try { runtimeDirectory = System.getProperty("runtimeDirectory"); if (runtimeDirectory == null) { runtimeDirectory = ""; } for (int i = 2;; i++) { // Try current directory, then "runtime2/", "runtime3/", etc. RandomAccessFile raf = new RandomAccessFile(runtimeDirectory + "lockfile.lck", "rw"); FileChannel fc = raf.getChannel(); pathLock = fc.tryLock(); if (pathLock != null) { break; } runtimeDirectory = "runtime" + i + "/"; // Construct a subdirectory name "runtime2/", "runtime3/", etc. } } catch (Exception ex) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "Can''t lock directory {0}. please either create the directory or run the app fewer times.", runtimeDirectory); Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, ex.getLocalizedMessage()); System.exit(1); } Logger.getLogger(Mediator.class.getName()).log(Level.INFO, "Using runtimeDirectory {0}", runtimeDirectory); } static public String getRuntimeDirectory() { return runtimeDirectory; } /** * Stops the OpenEMRConnect library services. * * This routine should be called for an orderly shut-down * of the Mediator library. * <p> * Call this method last, after you are through using the services. */ public void stop() { Logger.getLogger(Mediator.class.getName()).log(Level.INFO, "OpenEMRConnect library services stopped."); queueManager.stop(); httpService.stop(); } /** * Supresses the use of the logging service to send messages to the logging server. * This method is intended for use by the logging service itself, * so it won't try to send all log entries to itself. */ public static void suppressLoggingService() { useLoggingService = false; } /** * Sets the logging level according to the properties file. * The logging level is also set in the root handler if it is not the default. * Otherwise, new loggers will not be able to log anything at a * lower level than the default level. */ private static void setLoggerLevel() { if (loggerLevel == null) { loggerLevel = Level.INFO; // Default unless changed below. String loggerLevelName = getProperty("Logger.Level"); if (loggerLevelName != null) { try { loggerLevel = Level.parse(loggerLevelName); } catch (IllegalArgumentException ex) { Logger.getLogger(Mediator.class.getName()).log(Level.WARNING, "Logger.Level property ''{0}'' not a valid logger level.", loggerLevelName); } if (loggerLevel != Level.INFO) { // Need to change default handler level? LogManager m = LogManager.getLogManager(); Logger rootLogger = m.getLogger(""); Handler rootHandler = rootLogger.getHandlers()[0]; rootHandler.setLevel(loggerLevel); } } } } /** * Tests to see if we should log something at a given level. * <p> * This method can be used to save the CPU time of a call to a logger. * It can be useful if the call to the logger itself may use a non-trivial amount of CPU. * For example, a logger call may invoke other methods to get some of the * arguments needed for the call. These tests need only be done if the * logger level is set low enough that the logger call will actually do something. * * @param testLevel Level to check if it would be logged. * @return true if this Level would be logged, otherwise false. */ public static boolean testLoggerLevel(Level testLevel) { boolean returnValue; if (loggerLevel == null) { returnValue = Level.INFO.intValue() <= testLevel.intValue(); } else { returnValue = loggerLevel.intValue() <= testLevel.intValue(); } return returnValue; } /** * Gets a standard java.util.logging.Logger, set to the logging level property, if any. * If the logging level property is not the default Level.INFO, the logging level * is also changed in the root logging handler currently defined. * * @param loggerName Name of the logger to create. * @return The Logger requested. */ public static Logger getLogger(String loggerName) { setLoggerLevel(); Logger logger = Logger.getLogger(loggerName); logger.setLevel(loggerLevel); return logger; } /** * Gets the standard properties class. * @return properties. * <p> * The default property file is named openemrconnect.properties. */ public static Properties getProperties() { if (properties == null) { properties = new Properties(); // If a system property defining the configuration directory is // available, use it. String configDirectory = System.getProperty("configDirectory"); if (configDirectory == null) { configDirectory = runtimeDirectory; } // First attempt to load from the filesystem, which will only work // in a dev environment (i.e. from within the IDE), and for many, // only for runtime2 or better. // On deployments, the properties file should live in the jar, and // thus require loading as a resource (unless the system property // for "configDirectory" is supplied). String propFileName = "openemrconnect.properties"; try { String propPathName = configDirectory + propFileName; Logger.getLogger(Mediator.class.getName()).log(Level.INFO, "Attempt property load from file ''{0}''", propPathName); try { FileInputStream fis = new FileInputStream(propPathName); properties.load(fis); fis.close(); } catch (IOException ex) { Logger.getLogger(Mediator.class.getName()).log(Level.INFO, "Attempt property load as resource ''{0}''", propFileName); InputStream in = (Mediator.class.getResourceAsStream(propFileName)); properties.load(in); in.close(); } } catch (Exception ex) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "getProperty() Can''t open ''{0}'' -- Please create the properties file if it doesn''t exist and then restart the app", propFileName); System.exit(1); } } return properties; } /** * Gets the value of a named property from the standard properties list. * * @param propertyName name of the property whose value we want * @return the value of the requested property, * or null if the property is not found. */ public static String getProperty(String propertyName) { if (properties == null) { getProperties(); } return properties.getProperty(propertyName); } /** * Registers a listener to receive data requests * * This method is used by server software to register an object supporting * the IService interface getData() method. When a message for this server * is received (and is not a response to an outstanding request), it will * be given to the getData() method of this object. * * @param callbackObject object implementing IService interface */ public static void registerCallback(IService callbackObject) { myCallbackObject = callbackObject; } /** * Makes a remote data request. This version is called by our user * to send a new data request to a server. * * @param requestTypeId type of request (see RequestType.java) * @param requestData object containing data for the request * @return object containing response data resulting from the request, or null if none */ public Object getData(int requestTypeId, Object requestData) { Message m = new Message(); m.setMessageData(requestData); m.setSourceAddress(getProperty("Instance.Address")); m.setSourceName(getProperty("Instance.Name")); /* * Determine the Type of message we are to send. */ MessageType messageType = MessageTypeRegistry.find(requestTypeId); m.setMessageType(messageType); if (messageType == null) { /* * This is most likely an error on the part of our caller. We were * called with a request type ID that is not found as a request * in our MessageType list. */ Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "getData() - Message type not found for Request type ID ''{0}''", requestTypeId); return null; } /* * If there is a response message type, then set responseExpected to true. * Note that if the message type is createPerson or modifyPerson, this * may be overridden by the caller's desire, below. */ if (messageType.getResponseMessageType() != null) { m.setResponseExpected(true); } else { m.setResponseExpected(false); } /* * Find the destination address and name. This is usually the default * destination for the message type. However if our caller is passing * us <code>PersonRequest</code> data, they may choose to explicitly * specify the destination rather than leaving it to the default. * Also, if we have a <code>PersonRequest</code>, then the caller * has the option of specifying an XML string to be used * instead of the standard template for the message. */ String defaultDestinationAddress = getProperty(messageType.getDefaultDestinationAddressProperty()); m.setDestinationAddress(defaultDestinationAddress); m.setDestinationName(messageType.getDefaultDestinationName()); String messageId = generateMessageId(); m.setMessageId(messageId); m.setToBeQueued(messageType.isToBeQueued()); if (requestData instanceof PersonRequest) { PersonRequest pr = (PersonRequest) requestData; if (pr.getDestinationAddress() != null) { m.setDestinationAddress(pr.getDestinationAddress()); } if (pr.getDestinationName() != null) { m.setDestinationName(pr.getDestinationName()); } m.setXml(pr.getXml()); if (pr.getRequestReference() != null) { m.setMessageId(pr.getRequestReference()); // Overwrite the auto-generated message ID. } if (!pr.isResponseRequested()) { MessageType.TemplateType templateType = messageType.getTemplateType(); if (templateType == MessageType.TemplateType.createPerson || templateType == MessageType.TemplateType.modifyPerson) { m.setResponseExpected(false); m.setToBeQueued(true); } } } Object returnData = null; if (m.getDestinationAddress() == null) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "getData() - Can''t find {0} in properties file.", messageType.getDefaultDestinationAddressProperty()); } else { /* * Send the request to the server. */ returnData = sendData(m); } return returnData; } /** * Constructs a new message ID to use for a message. The ID must be * unique for requests coming from this instance address (this * client or service running on this machine.) * <p> * We construct the message ID by putting two components together. The first * is the current time in milliseconds (since January 1, 1970.) The * second is a sequence number that starts at 0 when this process * started running. So even if we generate two message IDs without the * system time changing, they will be unique. * <p> * Note that if a message is a response to another message, it typically * uses the request message ID as its own, so the request and response * can be correlated. In this case, a new ID is not generated for the * response message. * * @return the new request ID. */ public static synchronized String generateMessageId() { long milliseconds = new Date().getTime(); return Long.toString(milliseconds) + Long.toString(messageSequenceNumber++); } /** * Sends data to a remote destination. This version is called internally, in one of two ways: * <p> * 1. From the public <code>getData</code> request, to continue processing a user * client request that needs to be sent to a server. * <p> * 2. From our received message handling. This is when an unsolicited request has * come here for a server that is bound to us. We have delivered the request to * the server. The server has given us back a response. And now we need to pack * up the response and return it to the client. * * @param m message to be sent * @return object containing response data from the request */ Object sendData(Message m) { Object returnData = null; MessageType messageType = m.getMessageType(); // For handy reference. NextHop nextHop = NextHop.getNextHopByAddress(m.getDestinationAddress()); m.setNextHop(nextHop); if (nextHop == null) { /* * This is an error in our routing mechanism. We have a desination * address, but we were unable to translate it into next hop information. */ Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "getData() - Next hop information not found for ''{0}'': {1}", new Object[]{m.getDestinationAddress(), m.summarize()}); return null; } /* * Pack the data into the XML message. */ String xml = xmlPacker.pack(m); m.setXml(xml); /* * If we may get a response to this message, add it to the list of responses we are expecting. */ MessagePendingQueue.Entry queueEntry = null; if (m.isResponseExpected()) { queueEntry = pendingQueue.enqueue(m); } /* * Send the message. */ m.setHopCount(1); // This will be the first hop. if (Mediator.testLoggerLevel(Level.FINE)) { Mediator.getLogger(Mediator.class.getName()).log(Level.FINE, "Sending message {0}", m.summarize()); } boolean messageSent = sendMessage(m); /* * If we expect a response to this message, wait for the response. * When we get the response, return it to our caller. If there is no * response message after a defined timeout period, return failure. */ if (m.isResponseExpected()) { Message responseMessage = null; if (messageSent) { responseMessage = pendingQueue.waitForResponse(queueEntry); } else { pendingQueue.dequeue(queueEntry); } MessageType.TemplateType templateType = m.getMessageType().getTemplateType(); switch (templateType) { case findPerson: case createPerson: case modifyPerson: //added above two lines in place of the two below to specify the message //templates that expect a response // case createPersonAccepted: // case modifyPersonAccepted: PersonResponse personResponse; if (responseMessage != null) { returnData = responseMessage.getMessageData(); personResponse = (PersonResponse) returnData; personResponse.setSuccessful(true); } else { personResponse = new PersonResponse(); personResponse.setSuccessful(false); } break; default: Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "sendData() Message with requestId {0} and templateType {1} is expecting a response but not handled.", new Object[]{m.getMessageType().getRequestTypeId(), templateType.name()}); break; } } /* * Return. */ return returnData; } /** * Process a received HTTP message. Either this is a message that is * destined for us, or we are an intermediate node that should forward the * message on its way to the next node. If we find that we have an * IP address to which we should forward this message, than forward it * on its way. Otherwise we will unpack and process it locally. * * @param m Message received */ void processReceivedMessage(Message m) { String destinationAddress = m.getDestinationAddress(); if (destinationAddress == null) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "Message has no destination address."); } else { String ourInstanceAddress = getProperty("Instance.Address"); if (destinationAddress.equalsIgnoreCase(ourInstanceAddress)) { // If the message is addressed to us: xmlPacker.unpack(m); if (m.getMessageData() == null) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "Received message did not unpack into messageData: {0}", m.summarize()); } else { if (m.getMessageData().getClass() == PersonRequest.class) { PersonRequest req = (PersonRequest) m.getMessageData(); req.setSourceAddress(m.getSourceAddress()); req.setSourceName(m.getSourceName()); req.setRequestReference(m.getMessageId()); req.setXml(m.getXml()); // Return raw XML through the API in case it is wanted. } else if (m.getMessageData().getClass() == PersonResponse.class) { PersonResponse rsp = (PersonResponse) m.getMessageData(); rsp.setSuccessful(true); rsp.setRequestReference(m.getMessageId()); } boolean responseDelivered = pendingQueue.findRequest(m); if (responseDelivered) { // Was the message a response to a request that we just delivered? if (Mediator.testLoggerLevel(Level.FINE)) { Mediator.getLogger(Mediator.class.getName()).log(Level.FINE, "Received message delivered as response to API: {0}", m.summarize()); } } else { if (Mediator.testLoggerLevel(Level.FINE)) { Mediator.getLogger(Mediator.class.getName()).log(Level.FINE, "Received message delivered unsolicited to API: {0}", m.summarize()); } processUnsolicitedMessage(m); } } } else { // If the message is not addressed to us: NextHop nextHop = NextHop.getNextHopByAddress(destinationAddress); if (nextHop == null) { /* * The message destination does not match our own, * and the router is not giving us next hop information. * This is a configuration error. */ Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "Next hop not found for received message {0}", m.summarize()); } else { /* * The message destination does not match our own, * and we have found an external IP address/port for it. * It is not destined for us, so we will pass it though * to its destination. */ m.setNextHop(nextHop); int hopCount = m.getHopCount(); hopCount++; m.setHopCount(hopCount); if (Mediator.testLoggerLevel(Level.FINE)) { Mediator.getLogger(Mediator.class.getName()).log(Level.FINE, "Relaying message {0}", m.summarize()); } sendMessage(m); } } } } /** * Process an unsolicited message. We have already determined that this * was not the response to a request that we were waiting for. * * @param m the unpacked message information. */ private void processUnsolicitedMessage(Message m) { MessageType messageType = m.getMessageType(); // For convenience below (code readability). if (messageType.getRequestTypeId() != 0) { // Does this message have a request ID? if (myCallbackObject != null) { // Yes. Did the user register a callback routine? CallbackThread c = new CallbackThread(this, myCallbackObject, m); Thread t = new Thread(c); t.start(); } else { /* * The user has not defined a callback routine. Meanwhile, someone sent us * an unsolicited message -- at least a message that was not a reply we were * waiting for. */ Logger.getLogger(Mediator.class.getName()).log(Level.WARNING, "Unsolicited message with request type {0} received. No user callback is registered: {1}", new Object[]{messageType.getRequestTypeId(), m.summarize()}); } } else { /* * We received a message that wasn't a response we were waiting for. Also, it * didn't have a request message type. * * This could happen if we sent a message for which we were expecting a response, * and meanwhile our program restarted, loosing the memory of which responses * we were expecting. Then the response finally came but we weren't expecting it. * * Or this could be an error of some sort. */ Logger.getLogger(Mediator.class.getName()).log(Level.WARNING, "Unsolicited message with XML root ''{0}'' received, but it isn''t registered as a request: {1}", new Object[]{messageType.getRootXmlTag(), m.summarize()}); } } /** * Sends a packed XML message. This is common code that is called * for any of the following reasons: * <p> * 1. Forward a received message that is not ultimately destined for us. * <p> * 2. Send a new request to a server. * <p> * 3. Send a response back from a server. * <p> * We check the hop count to make sure the message is not caught in a * routing loop. Then we see whether the message should be sent * with our without the queuing mechanism for storing and forwarding. * Then we send it. * * @param m Message to send * @return true if the message was queued or sent successfully (to the next hop), otherwise false */ private boolean sendMessage(Message m) { boolean messageSent = false; if (m.getHopCount() > MAX_HOP_COUNT) { /* * A message has been forwarded too many times, exceeding the maximum hop count. * This may indicate a routing loop between two or more systems. */ Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "sendMessage() - Hop count {0} exceeds maximum hop count {1} for destination ''{2}'', routed to ''{3}'': {4}", new Object[]{m.getHopCount(), MAX_HOP_COUNT, m.getDestinationAddress(), m.getNextHop().getIpAddressPort(), m.summarize()}); } else if (m.isToBeQueued()) { messageSent = queueManager.enqueue(m); } else { try { messageSent = httpService.send(m); // (toBeQueued = false) } catch (MalformedURLException ex) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "Error sending: " + m.summarize(), ex); } catch (IOException ex) { Logger.getLogger(Mediator.class.getName()).log(Level.SEVERE, "Error sending: " + m.summarize(), ex); } } return messageSent; } }