001/**
002The contents of this file are subject to the Mozilla Public License Version 1.1 
003(the "License"); you may not use this file except in compliance with the License. 
004You may obtain a copy of the License at http://www.mozilla.org/MPL/ 
005Software distributed under the License is distributed on an "AS IS" basis, 
006WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 
007specific language governing rights and limitations under the License. 
008
009The Original Code is "TestPanel.java".  Description: 
010"A user interface for testing communications with an HL7 server." 
011
012The Initial Developer of the Original Code is University Health Network. Copyright (C) 
0132001.  All Rights Reserved. 
014
015Contributor(s): ______________________________________. 
016
017Alternatively, the contents of this file may be used under the terms of the 
018GNU General Public License (the "GPL"), in which case the provisions of the GPL are 
019applicable instead of those above.  If you wish to allow use of your version of this 
020file only under the terms of the GPL and not to allow others to use your version 
021of this file under the MPL, indicate your decision by deleting  the provisions above 
022and replace  them with the notice and other provisions required by the GPL License.  
023If you do not delete the provisions above, a recipient may use your version of 
024this file under either the MPL or the GPL. 
025
026 */
027package ca.uhn.hl7v2.app;
028
029import java.awt.BorderLayout;
030import java.awt.Component;
031import java.awt.FlowLayout;
032import java.awt.event.ActionEvent;
033import java.awt.event.ActionListener;
034import java.awt.event.WindowAdapter;
035import java.awt.event.WindowEvent;
036import java.io.ByteArrayOutputStream;
037import java.io.File;
038import java.io.FileInputStream;
039import java.io.IOException;
040import java.io.InputStream;
041import java.nio.charset.Charset;
042import java.util.concurrent.ConcurrentLinkedQueue;
043
044import javax.swing.JButton;
045import javax.swing.JCheckBox;
046import javax.swing.JComboBox;
047import javax.swing.JFileChooser;
048import javax.swing.JFrame;
049import javax.swing.JLabel;
050import javax.swing.JList;
051import javax.swing.JOptionPane;
052import javax.swing.JPanel;
053import javax.swing.JScrollPane;
054import javax.swing.JSplitPane;
055import javax.swing.JTextArea;
056import javax.swing.SwingUtilities;
057
058import org.apache.log4j.BasicConfigurator;
059import org.apache.log4j.Layout;
060import org.apache.log4j.PatternLayout;
061import org.apache.log4j.WriterAppender;
062import org.apache.log4j.spi.LoggingEvent;
063import org.slf4j.Logger;
064import org.slf4j.LoggerFactory;
065
066import ca.uhn.hl7v2.HL7Exception;
067import ca.uhn.hl7v2.llp.LLPException;
068import ca.uhn.hl7v2.llp.LowerLayerProtocol;
069import ca.uhn.hl7v2.llp.MinLowerLayerProtocol;
070import ca.uhn.hl7v2.model.Message;
071import ca.uhn.hl7v2.parser.EncodingNotSupportedException;
072import ca.uhn.hl7v2.parser.GenericParser;
073import ca.uhn.hl7v2.parser.Parser;
074import ca.uhn.hl7v2.validation.impl.ValidationContextFactory;
075import ca.uhn.hl7v2.view.TreePanel;
076
077/**
078 * <p>
079 * A user interface for testing communications with an HL7 server. The UI
080 * contains a window with 4 main panels. One can enter message text into the
081 * first panel, press a "parse" button so that the message is displayed in a
082 * tree in the second panel, press "send" to send the message to a remote server
083 * and display the response in a tree in the third panel, and press "encode" to
084 * write the inbound message as text in the fourth panel. To use, run from the
085 * command line with no arguments, like this:
086 * </p>
087 * <p>
088 * <code>java -classpath . ca.uhn.hl7v2.app.TestPanel</code>
089 * </p>
090 * <p>
091 * Exceptions generated during parsing or server communication are logged in the
092 * working directory and displayed in a dialog box.
093 * </p>
094 * 
095 * @author Bryan Tripp
096 * @author Christian Ohr
097 * @deprecated This class has been replaced by the standalone TestPanel application. 
098 *      The new testpanel is located in a different HAPI module called "hapi-testpanel"  
099 */
100@SuppressWarnings("serial")
101public class TestPanel extends JPanel implements ConnectionListener {
102
103        private static final Logger log = LoggerFactory.getLogger(TestPanel.class);
104
105        private static HL7Service service;
106
107        private GenericParser parser;
108        private JTextArea outboundText;
109        private TreePanel outboundTree;
110        private JTextArea inboundText;
111        private TreePanel inboundTree;
112        private JSplitPane messages;
113        private JList connList = null;
114        private MessageTypeRouter router;
115        private JCheckBox xmlCheckBox;
116        private JFileChooser fc;
117        private JLabel connectionListening;
118        private JTextArea events;
119        private SwingLogAppender appender;
120        private boolean started;
121        private ConnectionHub connectionHub;
122
123        /** Creates a new instance of TestPanel */
124        public TestPanel() throws HL7Exception {
125
126                this.parser = new GenericParser();
127                // Relax all validation
128                this.parser.setValidationContext(ValidationContextFactory.noValidation());
129                connectionHub = ConnectionHub.getInstance();
130                router = new MessageTypeRouter();
131                initUI();
132                BasicConfigurator.configure(appender);
133        }
134
135        /**
136         * Creates and lays out UI components
137         */
138        private void initUI() {
139                // main part of panel shows grid of 4 message areas: outbound on top;
140                // inbound on bottom
141                // and plain text on left, tree on right
142                this.setLayout(new BorderLayout());
143                outboundText = new JTextArea(10, 10);
144                JScrollPane outTextScroll = new JScrollPane(outboundText);
145                outboundTree = new TreePanel();
146                JScrollPane outTreeScroll = new JScrollPane(outboundTree);
147                inboundText = new JTextArea(10, 10);
148                JScrollPane inTextScroll = new JScrollPane(inboundText);
149                inboundTree = new TreePanel();
150                JScrollPane inTreeScroll = new JScrollPane(inboundTree);
151
152                JSplitPane outbound = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true,
153                                addTitle(outTextScroll, " Outbound Message Text "), addTitle(
154                                                outTreeScroll, " Outbound Message Tree "));
155                JSplitPane inbound = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true,
156                                addTitle(inTextScroll, " Inbound Message Text "), addTitle(
157                                                inTreeScroll, " Inbound Message Tree "));
158                messages = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, outbound,
159                                inbound);
160
161                events = new JTextArea(10, 10);
162                JScrollPane eventsScroll = new JScrollPane(events);
163                appender = new SwingLogAppender(events);
164                JSplitPane messagesAndLog = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
165                                true, messages, eventsScroll);
166                this.add(messagesAndLog, BorderLayout.CENTER);
167
168                // controls arranged along top for now
169                JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
170                final JComboBox charsetChooser = new JComboBox(Charset
171                                .availableCharsets().keySet().toArray(new String[0]));
172                charsetChooser.setSelectedItem(Charset.defaultCharset().displayName());
173                charsetChooser.setMaximumRowCount(15);
174                JButton loadButton = new JButton(" Load ");
175                JButton clearButton = new JButton(" Clear ");
176                JButton parseButton = new JButton(" Parse ");
177                JButton sendButton = new JButton(" Send ");
178                JButton encodeButton = new JButton(" Encode Inbound ");
179                JButton encodeOriginalButton = new JButton(" Encode Outbound ");
180                // JButton xmlEncodeButton = new JButton(" Encode XML ");
181                xmlCheckBox = new JCheckBox("Use XML", false);
182                JButton connectButton = new JButton(" Connect ");
183                JButton disconnectButton = new JButton(" Disconnect ");
184                controlPanel.add(charsetChooser);
185                controlPanel.add(loadButton);
186                controlPanel.add(clearButton);
187                controlPanel.add(parseButton);
188                controlPanel.add(encodeButton);
189                controlPanel.add(encodeOriginalButton);
190                controlPanel.add(xmlCheckBox);
191                controlPanel.add(connectButton);
192                controlPanel.add(sendButton);
193                controlPanel.add(disconnectButton);
194                this.add(controlPanel, BorderLayout.NORTH);
195
196                // connection selector on right side
197                connList = new JList();
198                connList.setPrototypeCellValue("xxxxxxxxxxxxxxxxxxxxxxxxxxx");
199                connList.setSelectionMode(0); // found through trial & error - don't
200                // know where constants are defined
201                JPanel connPanel = new JPanel(new BorderLayout());
202                connPanel.add(new JScrollPane(connList), BorderLayout.CENTER);
203                connectionListening = new JLabel(" Connections ");
204                connPanel.add(connectionListening, BorderLayout.NORTH);
205                this.add(connPanel, BorderLayout.EAST);
206
207                fc = new JFileChooser();
208
209                // set up event handlers for buttons
210                loadButton.addActionListener(new ActionListener() {
211                        public void actionPerformed(ActionEvent ae) {
212                                try {
213                                        int returnVal = fc.showOpenDialog(getThis());
214                                        if (returnVal == JFileChooser.APPROVE_OPTION) {
215                                                File file = fc.getSelectedFile();
216                                                // This is where a real application would open the file.
217                                                String input = readFile(file, Charset
218                                                                .forName((String) charsetChooser
219                                                                                .getSelectedItem()));
220                                                outboundText.setText(input);
221                                                outboundText.setCaretPosition(0);
222                                        }
223
224                                } catch (Exception e) {
225                                        showException(e);
226                                }
227                        }
228                });
229
230                // set up event handlers for buttons
231                clearButton.addActionListener(new ActionListener() {
232                        public void actionPerformed(ActionEvent ae) {
233                                try {
234                                        outboundText.setText("");
235                                        inboundText.setText("");
236                                        inboundTree.setMessage(null);
237                                        outboundTree.setMessage(null);
238                                        events.setText("");
239
240                                } catch (Exception e) {
241                                        showException(e);
242                                }
243                        }
244                });
245
246                // set up event handlers for buttons
247                parseButton.addActionListener(new ActionListener() {
248                        public void actionPerformed(ActionEvent ae) {
249                                try {
250                                        parseOutbound();
251                                } catch (Exception e) {
252                                        showException(e);
253                                }
254                        }
255                });
256
257                connectButton.addActionListener(new ActionListener() {
258                        public void actionPerformed(ActionEvent ae) {
259                                try {
260                                        new ConnectionDialog(getThis());
261                                } catch (Exception e) {
262                                        log.error(e.getMessage());
263                                        showException(e);
264                                }
265                        }
266                });
267
268                sendButton.addActionListener(new ActionListener() {
269                        public void actionPerformed(ActionEvent ae) {
270                                try {
271                                        sendAndRecieve();
272                                } catch (Exception e) {
273                                        showException(e);
274                                }
275                        }
276                });
277
278                encodeButton.addActionListener(new ActionListener() {
279                        public void actionPerformed(ActionEvent ae) {
280                                try {
281                                        encodeInbound();
282                                } catch (Exception e) {
283                                        showException(e);
284                                }
285                        }
286                });
287
288                encodeOriginalButton.addActionListener(new ActionListener() {
289                        public void actionPerformed(ActionEvent ae) {
290                                try {
291                                        encodeOutbound();
292                                } catch (Exception e) {
293                                        showException(e);
294                                }
295                        }
296                });
297
298                xmlCheckBox.addActionListener(new ActionListener() {
299                        public void actionPerformed(ActionEvent ae) {
300                                if (xmlCheckBox.isSelected()) {
301                                        parser.setXMLParserAsPrimary();
302                                } else {
303                                        parser.setPipeParserAsPrimary();
304                                }
305                        }
306                });
307
308                disconnectButton.addActionListener(new ActionListener() {
309                        public void actionPerformed(ActionEvent ae) {
310                                try {
311                                        disconnect(getCurrentConnection());
312                                } catch (Exception e) {
313                                        showException(e);
314                                }
315                        }
316                });
317
318        }
319
320        private String readFile(File file, Charset charset) throws IOException {
321                byte[] buffer = new byte[4 * 1024];
322                ByteArrayOutputStream baos = new ByteArrayOutputStream();
323
324                InputStream in = null;
325                try {
326                        in = new FileInputStream(file);
327                        int len = in.read(buffer);
328                        while (len >= 0) {
329                                baos.write(buffer, 0, len);
330                                len = in.read(buffer);
331                        }
332                        return new String(baos.toString(charset.name()));
333                } catch (IOException e) {
334                        log.error("Failed reading file {}", file.getAbsolutePath(), e);
335                        throw e;
336                } finally {
337                        if (in != null)
338                                try {
339                                        in.close();
340                                } catch (IOException e) {
341                                        // left empty
342                                }
343                }
344
345        }
346
347        /** Used in connect button handler ... got a better idea? */
348        private TestPanel getThis() {
349                return this;
350        }
351
352        /**
353         * Returns this TestPanel's underlying GenericParser. This method is needed
354         * by the HL7Service that the panel uses, so that it can share the parser
355         * and use whatever encoding it is using.
356         */
357        protected Parser getParser() {
358                return this.parser;
359        }
360
361        /**
362         * Adds a title to a component by putting it in another panel, adding the '
363         * title, and returning the new panel.
364         */
365        private static JPanel addTitle(Component toBeTitled, String title) {
366                JPanel newPanel = new JPanel(new BorderLayout());
367                newPanel.add(toBeTitled, BorderLayout.CENTER);
368                newPanel.add(new JLabel(title), BorderLayout.NORTH);
369                return newPanel;
370        }
371
372        /**
373         * Displays an exception in a standard way. All exceptions for TestPanel are
374         * routed here. Currently this prints to std err, and displays an error
375         * dialog, but it could be changed.
376         */
377        public void showException(Exception e) {
378                JOptionPane.showMessageDialog(this, e.getMessage(), e.getClass()
379                                .getName(), JOptionPane.ERROR_MESSAGE);
380                log.error("showException: ", e);
381        }
382
383        /**
384         * Attempts to parse the contents of the outbound message text box into a
385         * message object and display in the outbound message tree.
386         */
387        public void parseOutbound() throws HL7Exception,
388                        EncodingNotSupportedException {
389                // replace all the line feeds in the text area with carriage returns ...
390                String messageString = this.outboundText.getText().replace('\n', '\r');
391                Message out = parser.parse(messageString);
392                this.outboundTree.setMessage(out);
393
394                if (messages.getDividerLocation() < 0)
395                        messages.setDividerLocation(0.5);
396                this.validate();
397        }
398
399        /**
400         * Returns the Connection that is currently selected. If the connection has
401         * been closed by the remote peer, the method attempts to reopen it.
402         * 
403         * @throws LLPException
404         * @throws IOException
405         */
406        private ConnectionData getCurrentConnection() throws Exception {
407                ConnectionData ce = null;
408                Object o = connList.getSelectedValue();
409                if (o != null) {
410                        ce = (ConnectionData) o;
411                        if (!connectionHub.isOpen(ce)) {
412                                disconnect(ce);
413                                connect(ce);
414                        }
415                }
416                return ce;
417        }
418
419        /**
420         * Sets up a connection to a remote server that uses the minimal lower layer
421         * protocol, and this TestPanel's GenericParser.
422         */
423        public void connect(String host, int port) throws Exception {
424                connect(host, port, false);
425        }
426
427        /**
428         * Sets up a connection to a remote server that uses the minimal lower layer
429         * protocol, and this TestPanel's GenericParser. // TODO ConnectionHub?
430         */
431        public void connect(String host, int port, boolean tls) throws Exception {
432                Connection c = connectionHub.attach(host, port, parser,
433                                MinLowerLayerProtocol.class, tls);
434                c.getResponder().registerApplication(this.router);
435                connList.setListData(connectionHub.allConnections().toArray(
436                                new ConnectionData[0]));
437        }
438
439        /**
440         * Sets up a connection to a remote server that uses the minimal lower layer
441         * protocol, and this TestPanel's GenericParser.
442         */
443        public void connect(String host, int inboundPort, int outboundPort)
444                        throws Exception {
445                connect(host, inboundPort, outboundPort, false);
446        }
447
448        /**
449         * Sets up a connection to a remote server that uses the minimal lower layer
450         * protocol, and this TestPanel's GenericParser.
451         */
452        public void connect(String host, int inboundPort, int outboundPort,
453                        boolean tls) throws Exception {
454                Connection c = connectionHub.attach(host, outboundPort, inboundPort,
455                                parser, MinLowerLayerProtocol.class, tls);
456                c.getResponder().registerApplication(this.router);
457                connList.setListData(connectionHub.allConnections().toArray(
458                                new ConnectionData[0]));
459        }
460
461        /**
462         * Sets up a connection to a remote server that uses the minimal lower layer
463         * protocol, and this TestPanel's GenericParser.
464         */
465        public void connect(ConnectionData cf) throws Exception {
466                Connection c = connectionHub.attach(cf);
467                c.getResponder().registerApplication(this.router);
468                connList.setListData(connectionHub.allConnections().toArray(
469                                new ConnectionData[0]));
470        }
471
472        /** Notification that a new Connection has arrived at an HL7Service. */
473        public void connectionReceived(ca.uhn.hl7v2.app.Connection connection) {
474                connection.getResponder().registerApplication(this.router);
475        }
476
477        public void connectionDiscarded(Connection connection) {
478        }
479
480        private void disconnect(ConnectionData c) {
481                if (c != null) {
482                        connectionHub.detach(connectionHub.getKnownConnection(c));
483                        connList.setListData(connectionHub.allConnections().toArray(
484                                        new ConnectionData[0]));
485                }
486        }
487
488        /**
489         * Returns the MessageTypeRouter associated with this TestPanel. Every
490         * Connection that a TestPanel uses routes unsolicited messages through this
491         * MessageTypeRouter. Applications can be registered with the router using
492         * registerApplication().
493         */
494        public MessageTypeRouter getRouter() {
495                return this.router;
496        }
497
498        /**
499         * Sends the message that is currently displayed in the outbound tree to the
500         * remote system that is currently connected.
501         */
502        public void sendAndRecieve() throws Exception {
503                Message outbound = this.outboundTree.getMessage();
504                Message inbound;
505                try {
506                        inbound = connectionHub.getKnownConnection(getCurrentConnection())
507                                        .getInitiator().sendAndReceive(outbound);
508                } catch (NullPointerException e) {
509                        throw new IOException("Please select a Connection.");
510                }
511                this.inboundTree.setMessage(inbound);
512                this.validate();
513        }
514
515        /**
516         * Encodes the message that is currently displayed in the tree into a
517         * traditionally encoded message string and displays in the inbound message
518         * text box.
519         */
520        public void encodeInbound() throws HL7Exception {
521                String inbound = this.parser.encode(this.inboundTree.getMessage());
522                inbound = inbound.replace('\r', '\n');
523                this.inboundText.setText(inbound);
524        }
525
526        /**
527         * Encodes the message that is currently displayed in the outbound tree into
528         * a traditionally encoded message string and displays in a new window.
529         */
530        public void encodeOutbound() throws HL7Exception {
531                String outbound = this.parser.encode(this.outboundTree.getMessage());
532                outbound = outbound.replace('\r', '\n');
533                TestPanel.openTextWindow("Outbound Message", outbound);
534        }
535
536        /**
537         * Encodes the message that is currently displayed in the tree into an XML
538         * encoded message string and displays in the inbound message text box.
539         */
540        /*
541         * public void xmlEncodeInbound() throws HL7Exception { String inbound =
542         * this.xparser.encode(this.inboundTree.getMessage());
543         * this.inboundText.setText(inbound); }
544         */
545
546        /**
547         * Opens a new window for displaying text (intended for displaying encoded
548         * messages.
549         */
550        public static void openTextWindow(String title, String text) {
551                JFrame frame = new JFrame(title);
552
553                try {
554                        frame.getContentPane().setLayout(new BorderLayout());
555                        JTextArea textArea = new JTextArea(text);
556                        JScrollPane scroll = new JScrollPane(textArea);
557                        frame.getContentPane().add(scroll, BorderLayout.CENTER);
558
559                        frame.pack();
560                        frame.setVisible(true);
561                } catch (Exception e) {
562                        System.err.println("Can't display text in new window: "
563                                        + e.getMessage());
564                }
565        }
566
567        class SwingLogAppender extends WriterAppender {
568
569                private final JTextArea area;
570                private ConcurrentLinkedQueue<String> buf;
571
572                public SwingLogAppender(JTextArea area) {
573                        super();
574                        this.area = area;
575                        this.buf = new ConcurrentLinkedQueue<String>();
576                        setLayout(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN));
577                }
578
579                @Override
580                public void append(LoggingEvent event) {
581                        buf.offer(getLayout().format(event));
582
583                        if (getLayout().ignoresThrowable()) {
584                                String[] exception = event.getThrowableStrRep();
585                                if (exception != null) {
586                                        for (String line : exception) {
587                                                buf.offer(line);
588                                                buf.offer(Layout.LINE_SEP);
589                                        }
590                                }
591                        }
592                        display();
593                }
594
595                void display() {
596                        if (TestPanel.this.started && !buf.isEmpty()) {
597                                SwingUtilities.invokeLater(new Runnable() {
598
599                                        public void run() {
600                                                while (!buf.isEmpty()) {
601                                                        area.append(buf.poll());
602                                                }
603                                                buf.clear();
604                                        }
605
606                                });
607                        }
608                }
609
610        }
611
612        public static void main(String args[]) {
613
614                // BasicConfigurator.configure();
615                if (args.length > 3) {
616                        System.out
617                                        .println("Usage: ca.uhn.hl7v2.app.TestPanel [tls] [inbound_port [outbound_port] ]");
618                        System.out
619                                        .println("    If port numbers are provided, an HL7Server will be started, to listen for incoming connections.");
620                        System.out
621                                        .println("    If outbound port is not provided, inbound and outbound messages will use the same port.");
622                        System.out
623                                        .println("    If tls is provided, inbound and outbound messages will be received/sent over TLS.");
624                        System.exit(1);
625                }
626
627                // show a TestPanel in a window
628                JFrame frame = new JFrame("Message Tester");
629
630                try {
631                        TestPanel panel = new TestPanel();
632                        boolean tls = false;
633                        int i = 0;
634
635                        try {
636                                if (args.length > 0) {
637                                        LowerLayerProtocol llp = LowerLayerProtocol.makeLLP();
638
639                                        if ("tls".equals(args[i]) || "ssl".equals(args[i])) {
640                                                tls = true;
641                                                i++;
642                                        }
643
644                                        int inPort = Integer.parseInt(args[i++]);
645                                        if (args.length > i) {
646                                                int outPort = Integer.parseInt(args[i]);
647                                                service = new TwoPortService(panel.getParser(), llp,
648                                                                inPort, outPort, tls);
649                                        } else {
650                                                service = new SimpleServer(inPort, llp,
651                                                                panel.getParser(), tls);
652                                        }
653                                        service.registerConnectionListener(panel);
654                                        service.start();
655                                }
656
657                        } catch (NumberFormatException nfe) {
658                                System.out
659                                                .println("The given port number(s) are not valid integers");
660                                System.exit(1);
661                        } catch (Exception e) {
662                                e.printStackTrace();
663                        }
664
665                        frame.getContentPane().add(panel, BorderLayout.CENTER);
666
667                        // Finish setting up the frame
668                        frame.addWindowListener(new WindowAdapter() {
669                                public void windowClosing(WindowEvent e) {
670                                        System.exit(0);
671                                }
672                        });
673
674                        frame.pack();
675                        frame.setVisible(true);
676                        panel.started = true;
677                        panel.appender.display();
678                } catch (Exception e) {
679                        e.printStackTrace();
680                }
681        }
682
683}