/* ***** 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 java.io.IOException; import java.net.MalformedURLException; import java.util.logging.Level; import java.util.logging.Logger; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; import java.sql.ResultSet; import java.util.HashSet; /** * Offers a store-and-forward queueing facility to send messages to a * network destination. If the message cannot be sent immediately, it is * queued for later sending. The Queue Manager periodically tries to * send queued messages to their respective destinations. * * @author Scott Davis * @author Jim Grace */ class QueueManager implements Runnable { /* HTTP Handler object we can call to send HTTP messages. */ private HttpService httpService; /* Indication whether we have started a polling thread. */ private boolean pollingThreadStarted = false; /* Indication if it is time to shut down -- stop() has been called. */ private boolean timeToShutDown = false; /* Disable Queue Manager functions (just pass through -- used for debugging) */ private boolean queueManagerDisabled = false; /* Interval at which to retry sending queued messages. */ private int pollingInterval; //---------------------------------------------------------- // DATABASE CONNECTION VARIABLES // Includes Log-on information to connect to the database. //---------------------------------------------------------- /* The Connection to the database used to perform database operations. */ private Connection dataBaseConnection; /* * The connection protocol String to establish a database connection. * * In embedded mode(local connection), the protocol derby default is * "jdbc:derby:" plus the name of the data base. * * In client/server mode, the derby default is: * "jdbc:derby://localhost:" + PORT_NUMBER + "/". */ private String PROTOCOL = "jdbc:derby:"; /* * The name of the java database that will be created. It can be found * in a folder with the database name in the "derby.system.home" directory. */ private String DATABASE_NAME = "QUEUEMANAGER_DATABASE"; /* The name of the JavaDB table. */ private final String TABLE_NAME = "MESSAGE_SENDING_QUEUE"; /** * Constructor to set <code>HttpService</code> object for sending messages.</p> * * <p> It sets the link to the HttpSetvice object. Then it establishes a * Connection to a JavaDB Data Base. The Connection can be embedded or client. * If the Connection is created successfully, it creates a table and initializes * the connectionDownList.</p> * * @param httpService <code>HttpService</code> object for sending messages */ QueueManager(HttpService httpService) { this.httpService = httpService; // set polling interval String queueManagerPollingIntervalSeconds = Mediator.getProperty("QueueManager.PollingIntervalSeconds"); if (queueManagerPollingIntervalSeconds != null) { pollingInterval = 1000 * Integer.parseInt(queueManagerPollingIntervalSeconds); } else { pollingInterval = 10 * 60 * 1000; // Default polling interval of 10 minutes. } String disable = Mediator.getProperty("QueueManager.Disable"); if (disable != null && disable.trim().compareToIgnoreCase("true") == 0) { queueManagerDisabled = true; } else { //link the Connection to the database dataBaseConnection = establishDataBaseConnection(); if (dataBaseConnection != null) { //create the MESSAGE_SENDING_QUEUE table createTable(); } } } /** * Starts a polling thread if needed. * <p> * If the queue is empty, and a polling thread is not already running, * then we start a polling thread. */ synchronized void start() { if (!queueManagerDisabled && !isEmpty() && !timeToShutDown) { if (pollingThreadStarted) { this.notify(); } else { Thread t = new Thread(this); // // Programming note: set pollingThreadStarted to true before // we actually start the thread. That way if the thread exits // really quickly (setting pollingThreadStarted to false) // we won't set it back to true after it quits. // pollingThreadStarted = true; t.start(); } } } /** * Stops the queue manager (call after last use). */ synchronized void stop() { timeToShutDown = true; this.notify(); // Notify polling thread (if any) to wake up and shut down. } /** * Queues a message for sending. The message will be immediately added to * the queue on disk for safe-keeping, and then this method returns. * If there is network connectivity to the IP address, it will then be * sent immediately. If it fails to send, it will be periodically retried * until it succeeds. * * @param m Message to queue for sending */ public boolean enqueue(Message m) { boolean messageAdded = false; if (queueManagerDisabled) { try { messageAdded = httpService.send(m); // (toBeQueued = false) } catch (MalformedURLException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } catch (IOException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } } else { //prepare SQL Statement to perform the insertion String querySQL = "INSERT INTO " + TABLE_NAME + "( DESTINATION, XML_CODE, HOP_COUNT ) VALUES ( " + quote(m.getDestinationAddress()) + ", " + "?, " + m.getHopCount() + ")"; Logger.getLogger(QueueManager.class.getName()).log(Level.FINER, querySQL); try { PreparedStatement stmt = dataBaseConnection.prepareStatement(querySQL); int compressedXmlLenghth = m.getCompressedXmlLength(); byte[] blob = new byte[compressedXmlLenghth]; System.arraycopy(m.getCompressedXml(), 0, blob, 0, compressedXmlLenghth); stmt.setObject(1, blob); stmt.executeUpdate(); stmt.close(); messageAdded = true; // Log every incoming message (level FINE -- only one incoming log entry for each message.) Mediator.getLogger(QueueManager.class.getName()).log(Level.FINE, "Queued message to {0}", m.getDestinationAddress()); start(); // Start sending messages (like the one we just queued.) } catch (SQLException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } } return messageAdded; } /** * Tries to send all the Messages in the MESSAGE_SENDING_QUEUE. * If a send call is successful, then it removes the message from the * table. Otherwise, it puts the destination in a connectionDownList * so that it will not try to send Messages to a link that is down. * Message destinations on the connectionDownList are removed after * a waiting period so they can be retried. */ public void run() { String selectAllQuerySQL = "SELECT * FROM " + TABLE_NAME + " ORDER BY MESSAGE_ID"; try { Statement stmt = dataBaseConnection.createStatement(); ResultSet resultSet = stmt.executeQuery(selectAllQuerySQL); /* A HashSet that holds the destinations of the Messages that are not sent * successfully. Any Message with a destination on this list will * not try to be sent because it is assumed that the connection is down. * The list is cleared after a specified period of time. */ HashSet connectionDownList = new HashSet<String>(); //try to send each entry in that table while (resultSet.next()) { //Check to see if it is on the connectionDownList String destination = resultSet.getString("DESTINATION"); int messageId = resultSet.getInt("MESSAGE_ID"); if (connectionDownList.contains(destination)) { // It is on the connection down list, so do not try to send it. // Log this (level FINEST -- possibly many such logs for each message.) Mediator.getLogger(QueueManager.class.getName()).log(Level.FINEST, "Skipping queued message ID {0} to {1}", new Object[]{messageId, destination}); } else { Message m = new Message(); m.setDestinationAddress(destination); byte[] bytes = resultSet.getBytes("XML_CODE"); m.setCompressedXml(bytes); m.setCompressedXmlLength(bytes.length); m.setHopCount(resultSet.getInt("HOP_COUNT")); m.setToBeQueued(true); // We queued it, so next hop should also queue it. //initialize a boolean to hold the result of the send boolean sent = false; try { sent = httpService.send(m); } catch (MalformedURLException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } catch (IOException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } if (sent) { // Log every outgoing message (level FINE -- only one outgoing log entry for each message.) Mediator.getLogger(QueueManager.class.getName()).log(Level.FINE, "Sent queued message ID {0} to {1} via {2}", new Object[]{messageId, destination, m.getNextHop().getIpAddressPort()}); //it was sent correctly, remove it from the list delete(messageId); } else { // Log failed attempt (level FINEST -- possibly many such logs for each message.) Mediator.getLogger(QueueManager.class.getName()).log(Level.FINEST, "Failed to send queued message ID {0} to {1} via {2}", new Object[]{messageId, destination, m.getNextHop().getIpAddressPort()}); /* Put this destination on the connectionDownList so * resources are not wasted trying to send on a * connection that is down */ connectionDownList.add(resultSet.getString("DESTINATION")); } } } //close the SQL ResultSet and Statement resultSet.close(); stmt.close(); } catch (SQLException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } synchronized (this) { try { this.wait(pollingInterval); } catch (Exception ex) { } } synchronized (this) { pollingThreadStarted = false; start(); // Start another polling thread if queue is not empty. } } /** * Connects to the Java DataBase specified by the data base connection * variables. This connection is needed to work with the database table * MESSAGE_SENDING_QUEUE. The connection can be either embedded or client. * Be sure that the database connection variables are correct for the * program you are working with. */ private Connection establishDataBaseConnection() { Connection con = null; //load the driver try { //this driver is for embedded mode and loads "derby.jar" Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance(); } catch (ClassNotFoundException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } catch (InstantiationException ie) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ie); } catch (IllegalAccessException iae) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, iae); } try { //creates a new embedded Connection String strURL = PROTOCOL + Mediator.getRuntimeDirectory() + DATABASE_NAME + ";create=true;"; // Creates the database if it doesn't already exist. //swap these lines if you do not want to use a user name and password con = DriverManager.getConnection(strURL); // con = DriverManager.getConnection(strURL, USER_NAME, PASSWORD); } catch (SQLException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } return con; } /** * Creates the MESSAGE_SENDING_QUEUE JavaDataBase table using SQL. * This table keeps track of which Messages still need to be sent out. * The table only stores a few of the fields of the Message's: * DESTINATION, XML_CODE, and HOP_COUNT. */ private boolean createTable() { boolean tableOKforUse = false; //create an SQL Statement Statement stmt = null; String createTableQuery = "CREATE TABLE \"" + TABLE_NAME + "\"" + "(" + "MESSAGE_ID INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, " + "DESTINATION VARCHAR(100), " + "XML_CODE BLOB(30000), " + "HOP_COUNT INTEGER" + ")"; try { stmt = dataBaseConnection.createStatement(); stmt.execute(createTableQuery); stmt.close(); tableOKforUse = true; } catch (SQLException ex) { /* If the Exception is just saying that the table already exists, * that is ok and the table is still safe to use. Otherwise, * we want to report any other errors. */ if ("X0Y32".equals(ex.getSQLState())) { //this is the "table already exists" error tableOKforUse = true; } else { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } } return tableOKforUse; } /** * Deletes a Message from the MESSAGE_SENDING_QUEUE * * @param msgID message_id (table primary key) for message to delete */ private void delete(int msgID) { //prepare SQL statement to delete the Message Statement stmt = null; String deleteQuerySQL = "DELETE FROM " + TABLE_NAME + " WHERE MESSAGE_ID = " + msgID; Logger.getLogger(QueueManager.class.getName()).log(Level.FINER, deleteQuerySQL); //try to make the deletion try { stmt = dataBaseConnection.createStatement(); stmt.execute(deleteQuerySQL); stmt.close(); } catch (SQLException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } } /** * Tests to see if the queue is empty. * * @return true if the queue is empty, false if it is not empty. */ private boolean isEmpty() { boolean returnValue = true; //prepare SQL statement to print try { String selectAllQuerySQL = "SELECT 1 FROM " + TABLE_NAME; // Dummy select to see if there are any records. Statement stmt = dataBaseConnection.createStatement(); ResultSet resultSet = stmt.executeQuery(selectAllQuerySQL); returnValue = !resultSet.next(); // If next() is true, then isEmpty() is false, and visa versa resultSet.close(); stmt.close(); } catch (SQLException ex) { Logger.getLogger(QueueManager.class.getName()).log(Level.SEVERE, null, ex); } return returnValue; } /** * Quotes a string for use in a SQL statement. * Doubles single quotes (') and backslashes (\). * If the string is null, returns "null". * If the string is not null, returns the string with single quotes (') around it. * * @param s string to quote. * @return quoted string. */ public static String quote(String s) { if (s == null) { s = "null"; } else { s = s.replace("'", "''"); s = s.replace("\\", "\\\\"); s = "'" + s + "'"; } return s; } }//end class QueueManager