org.openvpms.hl7.impl.MessageDispatcherImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.openvpms.hl7.impl.MessageDispatcherImpl.java

Source

/*
 * Version: 1.0
 *
 * The contents of this file are subject to the OpenVPMS License Version
 * 1.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.openvpms.org/license/
 *
 * 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.
 *
 * Copyright 2014 (C) OpenVPMS Ltd. All Rights Reserved.
 */

package org.openvpms.hl7.impl;

import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.HapiContext;
import ca.uhn.hl7v2.app.Connection;
import ca.uhn.hl7v2.app.HL7Service;
import ca.uhn.hl7v2.llp.LLPException;
import ca.uhn.hl7v2.model.Message;
import ca.uhn.hl7v2.protocol.ReceivingApplication;
import ca.uhn.hl7v2.util.idgenerator.IDGenerator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openvpms.archetype.rules.practice.PracticeRules;
import org.openvpms.component.business.domain.im.act.DocumentAct;
import org.openvpms.component.business.domain.im.common.IMObjectReference;
import org.openvpms.component.business.domain.im.party.Party;
import org.openvpms.component.business.domain.im.security.User;
import org.openvpms.component.business.service.security.RunAs;
import org.openvpms.hl7.io.Connector;
import org.openvpms.hl7.io.Connectors;
import org.openvpms.hl7.io.MessageDispatcher;
import org.openvpms.hl7.io.MessageService;
import org.openvpms.hl7.io.Statistics;
import org.openvpms.hl7.util.HL7MessageStatuses;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * Default implementation of the {@link MessageDispatcher} interface.
 *
 * @author Tim Anderson
 */
public class MessageDispatcherImpl implements MessageDispatcher, DisposableBean, InitializingBean {

    /**
     * The message service.
     */
    private final MessageService messageService;

    /**
     * The connectors.
     */
    private final ConnectorsImpl connectors;

    /**
     * The practice rules.
     */
    private final PracticeRules rules;

    /**
     * The message header populator.
     */
    private final HeaderPopulator populator;

    /**
     * The message context.
     */
    private final HapiContext messageContext;

    /**
     * The message control ID generator.
     */
    private final IDGenerator generator;

    /**
     * The queues, keyed on connector reference.
     */
    private final Map<IMObjectReference, MessageQueue> queueMap = Collections
            .synchronizedMap(new HashMap<IMObjectReference, MessageQueue>());

    /**
     * The receivers, keyed on connector reference.
     */
    private final Map<IMObjectReference, MessageReceiver> receiverMap = Collections
            .synchronizedMap(new HashMap<IMObjectReference, MessageReceiver>());

    /**
     * The listeners, keyed on port.
     */
    private final Map<Integer, HL7Service> services = new HashMap<Integer, HL7Service>();

    /**
     * The service to schedule dispatching.
     */
    private final ExecutorService executor;

    /**
     * Listener for connector updates.
     */
    private final ConnectorsImpl.Listener listener;

    /**
     * Used to restricted the number of tasks that can be scheduled via the executor.
     */
    private Semaphore scheduled = new Semaphore(1);

    /**
     * Used to indicate that the dispatcher has been shut down.
     */
    private volatile boolean shutdown = false;

    /**
     * Used to wait if errors have occurred sending messages. This waits until a time expires or a message is queued.
     */
    private final Semaphore waiter = new Semaphore(0);

    /**
     * The user to initialise the security context in the dispatch thread.
     */
    private volatile User user;

    /**
     * Limits the no. of times an error is logged if the user isn't configured.
     */
    private boolean missingUser;

    /**
     * The logger.
     */
    private static final Log log = LogFactory.getLog(MessageDispatcherImpl.class);

    /**
     * Constructs a {@link MessageDispatcherImpl}.
     *
     * @param messageService the message service
     * @param connectors     the connectors
     * @param rules          the practice rules
     */
    public MessageDispatcherImpl(MessageService messageService, ConnectorsImpl connectors, PracticeRules rules) {
        this.messageService = messageService;
        this.connectors = connectors;
        this.rules = rules;
        populator = new HeaderPopulator();
        messageContext = HapiContextFactory.create();
        generator = messageContext.getParserConfiguration().getIdGenerator();
        executor = Executors.newSingleThreadExecutor();

        user = getServiceUser();

        listener = new ConnectorsImpl.Listener() {
            @Override
            public void added(Connector connector) {
                update(connector);
            }

            @Override
            public void removed(Connector connector) {
                remove(connector);
            }
        };
    }

    /**
     * Returns the message context.
     *
     * @return the message context
     */
    public HapiContext getMessageContext() {
        return messageContext;
    }

    /**
     * Queues a message to a connector.
     *
     * @param message   the message to queue
     * @param connector the connector
     * @param config    the message population configuration
     * @param user      the user responsible for the message
     * @return the queued message
     */
    @Override
    public DocumentAct queue(Message message, Connector connector, MessageConfig config, User user) {
        DocumentAct result;
        if (!(connector instanceof MLLPSender)) {
            throw new IllegalArgumentException("Unsupported connector: " + connector);
        }
        try {
            populate(message, (MLLPSender) connector, config);
            result = queue(message, (MLLPSender) connector, user);
        } catch (Throwable exception) {
            throw new IllegalStateException(exception);
        }
        return result;
    }

    /**
     * Resubmit a message.
     * <p/>
     * The associated connector must support message resubmission, and the message must have an
     * {@link HL7MessageStatuses#ERROR} status.
     *
     * @param message the message to resubmit
     */
    @Override
    public void resubmit(DocumentAct message) {
        messageService.resubmit(message);
        schedule();
    }

    /**
     * Registers an application to handle messages from the specified connector.
     * <p/>
     * Only one application can be registered to handle messages.
     *
     * @param connector   the connector
     * @param application the receiving application
     * @param user        the user responsible for messages received the connector
     * @throws IllegalStateException if the connector is registered
     */
    @Override
    public void listen(Connector connector, ReceivingApplication application, User user)
            throws InterruptedException {
        if (connector instanceof MLLPReceiver) {
            int port = ((MLLPReceiver) connector).getPort();
            HL7Service service;
            synchronized (services) {
                service = services.get(port);
                if (service == null || !service.isRunning()) {
                    service = messageContext.newServer(port, false);
                } else {
                    throw new IllegalStateException("Already receiving requests on port " + port);
                }
            }
            log.info("Starting listener for " + connector);
            MessageReceiver receiver = new MessageReceiver(application, connector, messageService, user);
            service.registerApplication(receiver);
            service.setExceptionHandler(receiver);
            service.startAndWait();
            boolean added = false;
            synchronized (services) {
                if (service.isRunning()) {
                    services.put(port, service);
                    added = true;
                } else {
                    log.error("Failed to start listener for " + connector);
                }
            }
            if (added) {
                receiverMap.put(connector.getReference(), receiver);
            }
        }
    }

    /**
     * Stops listening to messages from a connector.
     *
     * @param connector the connector
     */
    @Override
    public void stop(Connector connector) {
        if (connector instanceof MLLPReceiver) {
            int port = ((MLLPReceiver) connector).getPort();
            HL7Service service;
            synchronized (services) {
                service = services.remove(port);
            }
            receiverMap.remove(connector.getReference());
            if (service != null) {
                log.info("Stopping listener for " + connector);
                service.stopAndWait();
            }
        }
    }

    /**
     * Returns the statistics for a connector.
     *
     * @param connector the connector reference
     * @return the statistics, or {@code null} if the connector doesn't exist or is inactive
     */
    @Override
    public Statistics getStatistics(IMObjectReference connector) {
        Statistics statistics = queueMap.get(connector);
        if (statistics == null) {
            statistics = receiverMap.get(connector);
        }
        return statistics;
    }

    /**
     * Invoked by a BeanFactory after it has set all bean properties supplied
     * (and satisfied BeanFactoryAware and ApplicationContextAware).
     * <p/>
     * This delegates to {@link #initialise}
     */
    @Override
    public void afterPropertiesSet() {
        initialise();
    }

    /**
     * Initialises the dispatcher.
     */
    public void initialise() {
        initialise(connectors);
        connectors.addListener(listener);

        log.info("HL7 message dispatcher initialised");
        schedule();
    }

    /**
     * Invoked by a BeanFactory on destruction of a singleton.
     *
     * @throws Exception in case of shutdown errors.
     *                   Exceptions will get logged but not rethrown to allow
     *                   other beans to release their resources too.
     */
    @Override
    public void destroy() throws Exception {
        shutdown = true;
        connectors.removeListener(listener);
        executor.shutdown(); // Disable new tasks from being submitted
        waiter.release(); // wake from sleep
        try {
            // Wait a while for existing tasks to terminate
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow(); // Cancel currently executing tasks
                // Wait a while for tasks to respond to being cancelled
                if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                    log.error("Pool did not terminate");
                }
            }
        } catch (InterruptedException ie) {
            // (Re-)Cancel if current thread also interrupted
            executor.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }
        synchronized (services) {
            for (HL7Service service : services.values()) {
                service.stop();
            }
        }
    }

    /**
     * Creates a new timestamp for MSH-7.
     *
     * @return a new timestamp
     */
    protected Date createMessageTimestamp() {
        return new Date();
    }

    /**
     * Creates a new message control ID, for MSH-10.
     *
     * @return a new ID
     * @throws IOException if the ID cannot be generated
     */
    protected String createMessageControlID() throws IOException {
        return generator.getID();
    }

    /**
     * Initialise the connector queues.
     *
     * @param connectors the connectors
     */
    protected void initialise(Connectors connectors) {
        // initialise the queues
        for (Connector connector : connectors.getConnectors()) {
            if (connector instanceof MLLPSender) {
                getMessageQueue((MLLPSender) connector);
            }
        }
    }

    /**
     * Queues a message to a sender.
     *
     * @param message the message
     * @param sender  the sender
     * @param user    the user responsible for the message
     * @return the queued message
     * @throws HL7Exception if the message cannot be encoded
     */
    protected DocumentAct queue(Message message, final MLLPSender sender, User user) throws HL7Exception {
        DocumentAct result;
        MessageQueue queue = getMessageQueue(sender);
        if (log.isDebugEnabled()) {
            log.debug("queue() - " + sender);
        }
        result = queue.add(message, user);
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            // a transaction is in progress, so only invoke schedule() once the transaction has committed
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    schedule();
                }
            });
        } else {
            schedule();
        }
        return result;
    }

    /**
     * Sends a message, and returns the response.
     *
     * @param message the message to send
     * @param sender  the sender configuration
     * @return the response
     * @throws HL7Exception if the connection can not be initialised for any reason
     * @throws LLPException for any LLP error
     * @throws IOException  for any I/O error
     */
    protected Message send(Message message, MLLPSender sender) throws HL7Exception, LLPException, IOException {
        Message response;
        boolean debug = log.isDebugEnabled();
        long start = -1;
        if (debug) {
            log.debug("sending message via " + sender);
            log.debug(toString(message));
            start = System.currentTimeMillis();
        }
        Connection connection = null;
        try {
            connection = messageContext.newClient(sender.getHost(), sender.getPort(), false);
            int timeout = sender.getResponseTimeout();
            if (timeout <= 0) {
                timeout = MLLPSender.DEFAULT_RESPONSE_TIMEOUT;
            }
            connection.getInitiator().setTimeout(timeout, TimeUnit.SECONDS);
            response = connection.getInitiator().sendAndReceive(message);
            if (debug) {
                long end = System.currentTimeMillis();
                log.debug("response received in " + (end - start) + "ms");
                log.debug(toString(message));
            }
        } finally {
            if (connection != null) {
                connection.close();
            }
        }
        return response;
    }

    /**
     * Populates the header of a message.
     *
     * @param message the message
     * @param sender  the sender
     * @param config  the message population configuration
     * @throws HL7Exception for any error
     * @throws IOException  if a message control ID cannot be generated
     */
    private void populate(Message message, MLLPSender sender, MessageConfig config)
            throws HL7Exception, IOException {
        populator.populate(message, sender, createMessageTimestamp(), createMessageControlID(), config);
    }

    /**
     * Returns a queue for a sender, creating one if none exists.
     *
     * @param sender the sender
     * @return the queue
     */
    private MessageQueue getMessageQueue(MLLPSender sender) {
        MessageQueue queue;
        synchronized (queueMap) {
            queue = queueMap.get(sender.getReference());
            if (queue == null) {
                queue = new MessageQueue(sender, messageService, messageContext);
                queueMap.put(sender.getReference(), queue);
            }
        }
        return queue;
    }

    /**
     * Schedules {@link #dispatch()} to be run, unless it is already running.
     */
    private void schedule() {
        waiter.release(); // wakes up dispatch() if it is waiting

        final User user = getServiceUser();
        if (shutdown) {
            log.debug("MessageDispatcher shutting down. Schedule request ignored");
        } else if (user == null) {
            log.debug("No service user. Schedule request ignored");
        } else if (scheduled.tryAcquire()) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    scheduled.release(); // need to release here to enable dispatch() to reschedule
                    try {
                        RunAs.run(user, new Runnable() {
                            @Override
                            public void run() {
                                dispatch();
                            }
                        });
                    } catch (Throwable exception) {
                        log.error(exception.getMessage(), exception);
                    }
                }
            });
        } else {
            log.debug("MessageDispatcher already scheduled");
        }
    }

    /**
     * Sends all queued messages.
     */
    private void dispatch() {
        boolean processed;
        int waiting;
        long minWait;
        log.debug("dispatch() - start");
        do {
            processed = false;
            waiting = 0;
            minWait = 0;
            List<MessageQueue> queues = new ArrayList<MessageQueue>(queueMap.values());
            for (MessageQueue queue : queues) {
                // process each queue in a round robin fashion
                if (queue.isSuspended()) {
                    if (log.isDebugEnabled()) {
                        log.debug("dispatch() - skipping suspended queue " + queue);
                    }
                } else {
                    long wait = queue.getWaitUntil();
                    if (wait == -1 || wait <= System.currentTimeMillis()) {
                        processed |= sendFirst(queue);
                    } else {
                        ++waiting;
                        if (minWait == 0 || wait < minWait) {
                            minWait = wait;
                        }
                    }
                }
            }
        } while (processed && !shutdown);
        if (waiting != 0 && !shutdown) {
            long wait = minWait - System.currentTimeMillis();
            if (wait > 0) {
                log.debug("dispatch() waiting for " + wait + "ms");
                // wait until the minimum wait time has expired, or a message is queued
                try {
                    waiter.drainPermits();
                    waiter.tryAcquire(wait, TimeUnit.MILLISECONDS);
                } catch (InterruptedException ignore) {
                    // do nothing
                }
            }
            schedule();
        }
        log.debug("dispatch() - end");
    }

    /**
     * Sends the first message in a queue, if any are present.
     *
     * @param queue the queue
     * @return {@code true} if there was a message
     */
    protected boolean sendFirst(MessageQueue queue) {
        log.debug("sendFirst() - " + queue.getConnector());
        boolean processed = false;
        Message message = queue.peekFirst();
        if (message != null) {
            send(queue, message, queue.peekFirstAct());
            processed = true;
        } else {
            log.debug("sendFirst() - nothing to send");
        }
        return processed;
    }

    /**
     * Sends a message.
     *
     * @param queue   the message queue
     * @param message the message
     * @param act     the persistent act
     */
    protected void send(MessageQueue queue, Message message, DocumentAct act) {
        MLLPSender connector = queue.getConnector();
        try {
            Message response = send(message, connector);
            queue.sent(response);
        } catch (Throwable exception) {
            log.error("Failed to send message, act Id=" + act.getId(), exception);
            int retryInterval = connector.getRetryInterval();
            if (retryInterval <= 0) {
                retryInterval = MLLPSender.DEFAULT_RETRY_INTERVAL;
            }
            // failed to send the message, so don't queue for another retryInterval seconds
            queue.setWaitUntil(System.currentTimeMillis() + retryInterval * 1000);
            queue.error(exception);
        }
    }

    /**
     * Invoked when a connector is updated.
     *
     * @param connector the connector
     */
    private void update(Connector connector) {
        if (connector instanceof MLLPSender) {
            restartSender(connector);
        } else if (connector instanceof MLLPReceiver) {
            restartReceiver(connector);
        }
    }

    /**
     * Invoked to restart a receiver.
     *
     * @param connector the connector to use
     */
    private void restartReceiver(Connector connector) {
        MessageReceiver receiver = receiverMap.get(connector.getReference());
        if (receiver != null) {
            ReceivingApplication app = receiver.getReceivingApplication();
            stop(connector);
            try {
                listen(connector, app, receiver.getUser());
            } catch (InterruptedException exception) {
                log.error("Failed to update " + connector, exception);
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * Invoked to restart a sender.
     *
     * @param connector the connector
     */
    private void restartSender(Connector connector) {
        MessageQueue queue = queueMap.get(connector.getReference());
        if (queue != null) {
            log.info("Updating " + connector);
            queue.setConnector((MLLPSender) connector);
            queue.setWaitUntil(-1);
            schedule();
        }
    }

    /**
     * Invoked when a connector is removed or de-activated.
     *
     * @param connector the connector
     */
    private void remove(Connector connector) {
        if (connector instanceof MLLPSender) {
            MessageQueue queue = queueMap.remove(connector.getReference());
            if (queue != null) {
                // Note that a call to queue() could re-add the queue, even if it is inactive.
                log.info("Removed queue for " + connector);
                queue.setSuspended(true);
            }
        } else if (connector instanceof MLLPReceiver) {
            stop(connector);
        }
    }

    /**
     * Returns the user to set the security context in the dispatch thread.
     *
     * @return the user, or {@code null} if none has been configured
     */
    private User getServiceUser() {
        if (user == null) {
            synchronized (this) {
                if (user == null) {
                    Party practice = rules.getPractice();
                    if (practice != null) {
                        user = rules.getServiceUser(practice);
                    }
                    if (user == null && !missingUser) {
                        log.error("Missing party.organisationPractice serviceUser. Messages cannot be sent until "
                                + "this is configured");
                        missingUser = true;
                    }
                }
            }
        }
        return user;
    }

    /**
     * Formats a message for logging purposes.
     *
     * @param message the message
     * @return the formatted message
     */
    private String toString(Message message) {
        try {
            return HL7MessageHelper.toString(message);
        } catch (HL7Exception exception) {
            log.error(exception.getMessage(), exception);
            return "Failed to encode message";
        }
    }

}