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}