001 /**
002 The 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.
004 You may obtain a copy of the License at http://www.mozilla.org/MPL/
005 Software distributed under the License is distributed on an "AS IS" basis,
006 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
007 specific language governing rights and limitations under the License.
008
009 The Original Code is "TestPanel.java". Description:
010 "A user interface for testing communications with an HL7 server."
011
012 The Initial Developer of the Original Code is University Health Network. Copyright (C)
013 2001. All Rights Reserved.
014
015 Contributor(s): ______________________________________.
016
017 Alternatively, the contents of this file may be used under the terms of the
018 GNU General Public License (the "GPL"), in which case the provisions of the GPL are
019 applicable instead of those above. If you wish to allow use of your version of this
020 file only under the terms of the GPL and not to allow others to use your version
021 of this file under the MPL, indicate your decision by deleting the provisions above
022 and replace them with the notice and other provisions required by the GPL License.
023 If you do not delete the provisions above, a recipient may use your version of
024 this file under either the MPL or the GPL.
025
026 */
027 package ca.uhn.hl7v2.app;
028
029 import java.awt.BorderLayout;
030 import java.awt.Component;
031 import java.awt.FlowLayout;
032 import java.awt.event.ActionEvent;
033 import java.awt.event.ActionListener;
034 import java.awt.event.WindowAdapter;
035 import java.awt.event.WindowEvent;
036 import java.io.ByteArrayOutputStream;
037 import java.io.File;
038 import java.io.FileInputStream;
039 import java.io.IOException;
040 import java.io.InputStream;
041 import java.nio.charset.Charset;
042 import java.util.concurrent.ConcurrentLinkedQueue;
043
044 import javax.swing.JButton;
045 import javax.swing.JCheckBox;
046 import javax.swing.JComboBox;
047 import javax.swing.JFileChooser;
048 import javax.swing.JFrame;
049 import javax.swing.JLabel;
050 import javax.swing.JList;
051 import javax.swing.JOptionPane;
052 import javax.swing.JPanel;
053 import javax.swing.JScrollPane;
054 import javax.swing.JSplitPane;
055 import javax.swing.JTextArea;
056 import javax.swing.SwingUtilities;
057
058 import org.apache.log4j.BasicConfigurator;
059 import org.apache.log4j.Layout;
060 import org.apache.log4j.PatternLayout;
061 import org.apache.log4j.WriterAppender;
062 import org.apache.log4j.spi.LoggingEvent;
063 import org.slf4j.Logger;
064 import org.slf4j.LoggerFactory;
065
066 import ca.uhn.hl7v2.HL7Exception;
067 import ca.uhn.hl7v2.llp.LLPException;
068 import ca.uhn.hl7v2.llp.LowerLayerProtocol;
069 import ca.uhn.hl7v2.llp.MinLowerLayerProtocol;
070 import ca.uhn.hl7v2.model.Message;
071 import ca.uhn.hl7v2.parser.EncodingNotSupportedException;
072 import ca.uhn.hl7v2.parser.GenericParser;
073 import ca.uhn.hl7v2.parser.Parser;
074 import ca.uhn.hl7v2.validation.impl.ValidationContextFactory;
075 import 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")
101 public 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 }