View Javadoc

1   /**
2    * The contents of this file are subject to the Mozilla Public License Version 1.1
3    * (the "License"); you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at http://www.mozilla.org/MPL/
5    * Software distributed under the License is distributed on an "AS IS" basis,
6    * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
7    * specific language governing rights and limitations under the License.
8    *
9    * The Original Code is ""  Description:
10   * ""
11   *
12   * The Initial Developer of the Original Code is University Health Network. Copyright (C)
13   * 2001.  All Rights Reserved.
14   *
15   * Contributor(s): ______________________________________.
16   *
17   * Alternatively, the contents of this file may be used under the terms of the
18   * GNU General Public License (the  "GPL"), in which case the provisions of the GPL are
19   * applicable instead of those above.  If you wish to allow use of your version of this
20   * file only under the terms of the GPL and not to allow others to use your version
21   * of this file under the MPL, indicate your decision by deleting  the provisions above
22   * and replace  them with the notice and other provisions required by the GPL License.
23   * If you do not delete the provisions above, a recipient may use your version of
24   * this file under either the MPL or the GPL.
25   */
26  /*
27   * Created on October 17, 2001, 11:44 AM
28   */
29  package ca.uhn.hl7v2.testpanel.ui.v2tree;
30  
31  import java.awt.Color;
32  import java.awt.Component;
33  import java.awt.EventQueue;
34  import java.awt.Font;
35  import java.awt.event.KeyAdapter;
36  import java.awt.event.KeyEvent;
37  import java.beans.PropertyChangeEvent;
38  import java.beans.PropertyChangeListener;
39  import java.lang.reflect.InvocationTargetException;
40  import java.util.ArrayList;
41  import java.util.Collection;
42  import java.util.HashSet;
43  import java.util.List;
44  import java.util.Set;
45  
46  import javax.swing.DefaultListSelectionModel;
47  import javax.swing.Icon;
48  import javax.swing.ImageIcon;
49  import javax.swing.JScrollPane;
50  import javax.swing.JTable;
51  import javax.swing.JViewport;
52  import javax.swing.ListSelectionModel;
53  import javax.swing.SwingUtilities;
54  import javax.swing.event.CellEditorListener;
55  import javax.swing.event.ChangeEvent;
56  import javax.swing.table.DefaultTableCellRenderer;
57  import javax.swing.table.TableModel;
58  import javax.swing.tree.AbstractLayoutCache;
59  import javax.swing.tree.DefaultMutableTreeNode;
60  import javax.swing.tree.DefaultTreeModel;
61  import javax.swing.tree.TreeNode;
62  import javax.swing.tree.TreePath;
63  
64  import org.apache.commons.lang.StringUtils;
65  import org.apache.commons.lang.Validate;
66  import org.netbeans.swing.outline.DefaultOutlineModel;
67  import org.netbeans.swing.outline.Outline;
68  import org.netbeans.swing.outline.OutlineModel;
69  import org.netbeans.swing.outline.RenderDataProvider;
70  import org.netbeans.swing.outline.RowModel;
71  import org.slf4j.Logger;
72  import org.slf4j.LoggerFactory;
73  
74  import ca.uhn.hl7v2.HL7Exception;
75  import ca.uhn.hl7v2.conf.ProfileException;
76  import ca.uhn.hl7v2.conf.check.DefaultValidator;
77  import ca.uhn.hl7v2.model.AbstractGroup;
78  import ca.uhn.hl7v2.model.Composite;
79  import ca.uhn.hl7v2.model.Group;
80  import ca.uhn.hl7v2.model.Message;
81  import ca.uhn.hl7v2.model.Primitive;
82  import ca.uhn.hl7v2.model.Segment;
83  import ca.uhn.hl7v2.model.Structure;
84  import ca.uhn.hl7v2.model.Type;
85  import ca.uhn.hl7v2.model.Varies;
86  import ca.uhn.hl7v2.model.primitive.AbstractNumericPrimitive;
87  import ca.uhn.hl7v2.model.primitive.ID;
88  import ca.uhn.hl7v2.model.primitive.IS;
89  import ca.uhn.hl7v2.parser.EncodingCharacters;
90  import ca.uhn.hl7v2.parser.PipeParser;
91  import ca.uhn.hl7v2.testpanel.controller.Controller;
92  import ca.uhn.hl7v2.testpanel.model.UnknownMessage;
93  import ca.uhn.hl7v2.testpanel.model.conf.ConformanceComposite;
94  import ca.uhn.hl7v2.testpanel.model.conf.ConformanceGroup;
95  import ca.uhn.hl7v2.testpanel.model.conf.ConformanceMessage;
96  import ca.uhn.hl7v2.testpanel.model.conf.ConformancePrimitive;
97  import ca.uhn.hl7v2.testpanel.model.conf.ConformanceSegment;
98  import ca.uhn.hl7v2.testpanel.model.conf.TableFile;
99  import ca.uhn.hl7v2.testpanel.model.msg.AbstractMessage;
100 import ca.uhn.hl7v2.testpanel.model.msg.Comment;
101 import ca.uhn.hl7v2.testpanel.model.msg.Hl7V2MessageBase;
102 import ca.uhn.hl7v2.testpanel.model.msg.Hl7V2MessageCollection;
103 import ca.uhn.hl7v2.testpanel.ui.IDestroyable;
104 import ca.uhn.hl7v2.testpanel.ui.ImageFactory;
105 import ca.uhn.hl7v2.testpanel.ui.ShowEnum;
106 import ca.uhn.hl7v2.testpanel.util.SegmentAndComponentPath;
107 import ca.uhn.hl7v2.util.StringUtil;
108 import ca.uhn.hl7v2.validation.PrimitiveTypeRule;
109 import ca.uhn.hl7v2.validation.impl.DefaultValidation;
110 import ca.uhn.hl7v2.validation.impl.ValidationContextImpl;
111 
112 /**
113  * This is a Swing panel that displays the contents of a Message object in a
114  * JTree. The tree currently only expands to the field level (components shown
115  * as one node).
116  * 
117  * @author Bryan Tripp (bryan_tripp@sourceforge.net)
118  */
119 public class Hl7V2MessageTree extends Outline implements IDestroyable {
120 	private static final DefaultValidation ourDefaultValidation = new DefaultValidation();
121 
122 	private static final Logger ourLog = LoggerFactory.getLogger(Hl7V2MessageTree.class);
123 	private static final String TABLE_NAMESPACE_HL7 = "HL7";
124 	private static final String TBL = " ";
125 	private Controller myController;
126 	private boolean myCurrentlyEditing;
127 	private PropertyChangeListener myHighlitedPathListener;
128 	private PropertyChangeListener myMessageEncodingListener;
129 	private Hl7V2MessageCollection myMessages;
130 	private PropertyChangeListener myParsedMessagesListener;
131 	private PipeParser myPipeParser;
132 	private boolean myRespondingToManualRangeChange;
133 	private DefaultValidator myRuntimeProfileValidator;
134 	private boolean mySelectionHandlingDisabled;
135 	private boolean myShouldOpenDefaultPaths = true;
136 
137 	private boolean myShowRep0 = true;
138 
139 	private TreeRowModel myTableModel;
140 
141 	private TreeNodeRoot myTop;
142 
143 	private DefaultTreeModel myTreeModel;
144 
145 	private ShowEnum myUnitTestShowMode;
146 
147 	private UpdaterThread myUpdaterThread;
148 
149 	private PropertyChangeListener myValidationContextListener;
150 
151 	private IWorkingListener myWorkingListener;
152 
153 	/** Creates new TreePanel */
154 	public Hl7V2MessageTree(Controller theController) {
155 		addKeyListener(new KeyAdapter() {
156 			@Override
157 			public void keyPressed(KeyEvent e) {
158 				handleKeyPress(e);
159 			}
160 		});
161 
162 		setFont(new Font("LUCIDA", Font.PLAIN, 9));
163 
164 		myController = theController;
165 
166 		myPipeParser = new PipeParser();
167 		myPipeParser.setValidationContext(new ValidationContextImpl());
168 
169 		setRenderDataProvider(new TreeRenderDataProvider());
170 
171 		setShowGrid(true);
172 		setGridColor(new Color(0.9f, 0.9f, 0.9f));
173 		setRowHeight(16);
174 
175 		setRowSelectionAllowed(true);
176 
177 		setSelectionModel(new MySelectionModel());
178 
179 		ValueCellEditor valueCellEditor = new ValueCellEditor(getFont());
180 		setDefaultEditor(String.class, valueCellEditor);
181 
182 		valueCellEditor.addCellEditorListener(new CellEditorListener() {
183 
184 			public void editingCanceled(ChangeEvent theE) {
185 				ourLog.info("No longer editing");
186 				myCurrentlyEditing = false;
187 			}
188 
189 			public void editingStopped(ChangeEvent theE) {
190 				ourLog.info("No longer editing");
191 				myCurrentlyEditing = false;
192 			}
193 		});
194 
195 		myHighlitedPathListener = new PropertyChangeListener() {
196 
197 			public void propertyChange(PropertyChangeEvent theEvt) {
198 				if (myController.isMessageEditorInFollowMode()) {
199 					if (Hl7V2MessageTree.this.hasFocus() == false) {
200 						synchronizeTreeWithHighlitedPath();
201 					}
202 				}
203 			}
204 
205 		};
206 
207 		myParsedMessagesListener = new PropertyChangeListener() {
208 
209 			public void propertyChange(PropertyChangeEvent theEvt) {
210 				myUpdaterThread.scheduleUpdate();
211 			}
212 		};
213 
214 		myValidationContextListener = new PropertyChangeListener() {
215 
216 			public void propertyChange(PropertyChangeEvent theEvt) {
217 				myUpdaterThread.scheduleUpdate();
218 			}
219 		};
220 
221 		myMessageEncodingListener = new PropertyChangeListener() {
222 
223 			public void propertyChange(PropertyChangeEvent theEvt) {
224 				myUpdaterThread.scheduleUpdate();
225 			}
226 		};
227 
228 		getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
229 
230 		myUpdaterThread = new UpdaterThread();
231 		myUpdaterThread.start();
232 	}
233 
234 	private int addChidrenExtra(String theParentName, Type thePrimitive, TreeNodeBase treeParent, Segment theSegment, List<Integer> theComponentPath, String theTerserPath, int cpIndex, int index) throws InterruptedException, InvocationTargetException {
235 		// Extra components
236 		for (int i = 0; i < thePrimitive.getExtraComponents().numComponents(); i++) {
237 			Type nextType = thePrimitive.getExtraComponents().getComponent(i);
238 			String nextParentName = theParentName + "-" + (i + 1);
239 
240 			// theComponentPath.set(cpIndex, Integer.valueOf(i + 1));
241 			String terserPath = theTerserPath + "-" + (i + 1);
242 
243 			index = addChildren(nextParentName, treeParent, false, false, null, i, nextType, theSegment, theComponentPath, index, terserPath);
244 		}
245 		return index;
246 	}
247 
248 	void addChildren() throws InterruptedException, InvocationTargetException {
249 		if (myMessages != null && myMessages.getRuntimeProfile() != null) {
250 			myRuntimeProfileValidator = new DefaultValidator();
251 			myRuntimeProfileValidator.setValidateChildren(false);
252 		}
253 
254 		final Set<String> openPaths = getOpenPaths();
255 
256 		int selectedRow = getSelectedRow();
257 		final String selectedPath = getPathAtRow(selectedRow);
258 
259 		if (myMessages != null) {
260 			try {
261 				addChildren(myMessages.getMessages(), myTop, "");
262 			} catch (InterruptedException e) {
263 				ourLog.info("Interrupted during an update loop, going to schedule another pass");
264 				myUpdaterThread.scheduleUpdate();
265 			} catch (InvocationTargetException e) {
266 				ourLog.error("Failed up update message tree", e);
267 			}
268 
269 			myTop.validate();
270 
271 			EventQueue.invokeLater(new Runnable() {
272 				public void run() {
273 					myTreeModel.nodeStructureChanged(myTop);
274 				}
275 			});
276 
277 		}
278 
279 		EventQueue.invokeLater(new Runnable() {
280 			public void run() {
281 				try {
282 					mySelectionHandlingDisabled = true;
283 					ourLog.debug("Open paths are: {}", openPaths);
284 					if (openPaths.isEmpty() && myShouldOpenDefaultPaths) {
285 						ourLog.info("Opening default paths");
286 						final AbstractLayoutCache layout = ((OutlineModel) getModel()).getLayout();
287 						for (int row = 0; row < layout.getRowCount(); row++) {
288 							TreePath path = layout.getPathForRow(row);
289 							Object component = path.getLastPathComponent();
290 							if (component instanceof TreeNodeMessage || component instanceof TreeNodeUnknown || component instanceof TreeNodeGroup) {
291 								expandPath(path);
292 							}
293 						}
294 						myShouldOpenDefaultPaths = false;
295 					} else {
296 						ourLog.info("Opening pre-existing paths: {} and selected path: {}", openPaths, selectedPath);
297 						expandPaths(openPaths, selectedPath);
298 					}
299 				} finally {
300 					mySelectionHandlingDisabled = false;
301 				}
302 			}
303 		});
304 		// if (selectedRow != -1) {
305 		// getSelectionModel().setSelectionInterval(selectedRow, selectedRow);
306 		// handleNewSelectedIndex(selectedRow);
307 		// }
308 
309 	}
310 
311 	/**
312 	 * Adds the children of the given group under the given tree node.
313 	 */
314 	void addChildren(Group messParent, TreeNodeBase treeParent, String theTerserPath) throws InterruptedException, InvocationTargetException {
315 
316 		String[] childNames = messParent.getNames();
317 		int currChild = 0;
318 		for (int i = 0; i < childNames.length; i++) {
319 
320 			try {
321 				String nextName = childNames[i];
322 
323 				switch (getShowMode()) {
324 				case ALL:
325 				case ERROR:
326 					// case POPULATED:
327 					/*
328 					 * this creates at least one rep if there are none, since
329 					 * these modes want to show the node in the tree regardless
330 					 * of whether or not it has content
331 					 */
332 					messParent.get(nextName);
333 				default:
334 					// nothing
335 				}
336 
337 				Structure[] childReps = messParent.getAll(nextName);
338 				boolean repeating = messParent.isRepeating(nextName);
339 				boolean required = messParent.isRequired(nextName);
340 
341 				for (int j = 0; j < childReps.length; j++) {
342 
343 					TreeNodeBase newNode = null;
344 					Structure nextStructure = childReps[j];
345 					String groupName = nextStructure.getName();
346 
347 					String nextTerserPath = theTerserPath + "/" + groupName + (j > 0 ? ("(" + (j + 1) + ")") : "");
348 
349 					if (nextStructure instanceof Group) {
350 
351 						if (nextStructure instanceof ConformanceGroup) {
352 							newNode = new TreeNodeGroupConf((ConformanceGroup) nextStructure, groupName, j, repeating, required, nextTerserPath);
353 						} else {
354 							newNode = new TreeNodeGroup((Group) nextStructure, groupName, j, repeating, required, nextTerserPath);
355 						}
356 
357 						addChildren((Group) nextStructure, newNode, nextTerserPath);
358 
359 						newNode = insertOrReplaceWithExisting(treeParent, currChild, newNode);
360 
361 					} else if (nextStructure instanceof Segment) {
362 
363 						if (nextStructure instanceof ConformanceSegment) {
364 							newNode = new TreeNodeSegmentConf((ConformanceSegment) nextStructure, groupName, j, repeating, required, nextTerserPath);
365 						} else {
366 							newNode = new TreeNodeSegment((Segment) nextStructure, groupName, j, repeating, required, nextTerserPath);
367 						}
368 
369 						addChildren((Segment) nextStructure, newNode, nextTerserPath);
370 
371 						newNode = insertOrReplaceWithExisting(treeParent, currChild, newNode);
372 
373 					}
374 
375 					currChild++;
376 					// treeParent.insert(newNode, currChild++);
377 				}
378 			} catch (HL7Exception e) {
379 				ourLog.error("Failed to add group to tree", e);
380 			}
381 		}
382 	}
383 
384 	void addChildren(List<AbstractMessage<?>> theMessages, TreeNodeRoot theTop, String theTerserPath) throws InterruptedException, InvocationTargetException {
385 		int index = 0;
386 		for (AbstractMessage<?> abstractMessage : theMessages) {
387 
388 			if (abstractMessage instanceof Hl7V2MessageBase) {
389 
390 				Hl7V2MessageBase message = (Hl7V2MessageBase) abstractMessage;
391 				TreeNodeMessage node;
392 				if (message.getParsedMessage() instanceof ConformanceMessage) {
393 					node = new TreeNodeMessageConf(index, message);
394 				} else {
395 					node = new TreeNodeMessage(index, message);
396 				}
397 				insertOrReplaceWithExisting(theTop, index, node);
398 
399 				addChildren(node.getMessage().getParsedMessage(), node, "");
400 
401 			} else if (abstractMessage instanceof UnknownMessage) {
402 
403 				UnknownMessage unknownMessage = (UnknownMessage) abstractMessage;
404 				TreeNodeUnknown node = new TreeNodeUnknown(unknownMessage);
405 				insertOrReplaceWithExisting(theTop, index, node);
406 
407 				String message = unknownMessage.getParsedMessage();
408 				for (String line : message.split("(\\n|\\r)+")) {
409 					line = StringUtil.chomp(line);
410 					node.add(new TreeNodeUnknownLine(line));
411 				}
412 
413 			} else if (abstractMessage instanceof Comment) {
414 
415 				TreeNodeComment node = new TreeNodeComment((Comment) abstractMessage);
416 				insertOrReplaceWithExisting(theTop, index, node);
417 
418 			} else {
419 
420 				throw new IllegalStateException("Unknown type: " + abstractMessage.getClass());
421 
422 			}
423 
424 			index++;
425 		}
426 	}
427 
428 	/**
429 	 * Add fields of a segment to the tree ...
430 	 */
431 	void addChildren(Segment messParent, TreeNodeBase treeParent, String theTerserPath) throws InterruptedException, InvocationTargetException {
432 
433 		int n = messParent.numFields();
434 		String[] names = messParent.getNames();
435 		int index = 0;
436 		for (int i = 1; i <= n; i++) {
437 			try {
438 
439 				List<Integer> components = new ArrayList<Integer>();
440 				components.add(Integer.valueOf(i));
441 
442 				switch (getShowMode()) {
443 				case ALL:
444 				case ERROR:
445 					// case POPULATED:
446 					/*
447 					 * this creates at least one rep if there are none, since
448 					 * these modes want to show the node in the tree regardless
449 					 * of whether or not it has content
450 					 */
451 					messParent.getField(i, 0);
452 				default:
453 					// nothing
454 				}
455 
456 				Type[] reps = messParent.getField(i);
457 				boolean repeating = messParent.getMaxCardinality(i) != 1;
458 				boolean required = messParent.isRequired(i);
459 				String name = i <= names.length ? names[i - 1] : "Unknown";
460 
461 				for (int j = 0; j < reps.length; j++) {
462 
463 					// String field = PipeParser.encode(reps[j], encChars);
464 
465 					Type type = reps[j];
466 					String parentName = messParent.getName() + "-" + (i);
467 
468 					StringBuilder b = new StringBuilder();
469 					b.append(theTerserPath);
470 					b.append("-");
471 					b.append((i));
472 					if (repeating) {
473 						b.append('(');
474 						b.append(j + 1);
475 						b.append(')');
476 					}
477 					String terserPath = b.toString();
478 
479 					index = addChildren(parentName, treeParent, repeating, required, name, j, type, messParent, components, index, terserPath);
480 
481 				}
482 
483 			} catch (HL7Exception e) {
484 				ourLog.error("Failed to add child to tree", e);
485 			}
486 		}
487 	}
488 
489 	/**
490 	 * Adds components of a composite to the tree ...
491 	 * 
492 	 */
493 	void addChildren(String theParentName, Composite messParent, TreeNodeBase treeParent, Segment theSegment, List<Integer> theComponentPath, String theTerserPath) throws InterruptedException, InvocationTargetException {
494 		Type[] components = messParent.getComponents();
495 
496 		int cpIndex = theComponentPath.size();
497 		theComponentPath.add(null);
498 
499 		int index = 0;
500 		for (int i = 0; i < components.length; i++) {
501 			Type nextType = components[i];
502 			String nextParentName = theParentName + "-" + (i + 1);
503 
504 			theComponentPath.set(cpIndex, Integer.valueOf(i + 1));
505 			String terserPath = theTerserPath + "-" + (i + 1);
506 
507 			index = addChildren(nextParentName, treeParent, false, false, null, i, nextType, theSegment, theComponentPath, index, terserPath);
508 		}
509 
510 		index = addChidrenExtra(theParentName, messParent, treeParent, theSegment, theComponentPath, theTerserPath, cpIndex, index);
511 
512 		theComponentPath.remove(cpIndex);
513 
514 	}
515 
516 	int addChildren(String theParentName, TreeNodeBase theTreeParent, boolean theRepeating, boolean theRequired, String theName, int theRepNum, Type theType, Segment theParent, List<Integer> theComponentNumbers, int theIndex, String theTerserPath) throws InterruptedException,
517 			InvocationTargetException {
518 		if (theType instanceof Varies) {
519 			theType = ((Varies) theType).getData();
520 		}
521 
522 		if (theType instanceof Composite) {
523 			Composite composite = (Composite) theType;
524 			TreeNodeType newNode;
525 			if (composite instanceof ConformanceComposite) {
526 				newNode = new TreeNodeCompositeConf(theParentName, composite, theName, theRepNum, theRepeating, theRequired, theParent, theComponentNumbers, theTerserPath);
527 			} else {
528 				newNode = new TreeNodeType(theParentName, composite, theName, theRepNum, theRepeating, theRequired, theParent, theComponentNumbers, theTerserPath);
529 			}
530 
531 			addChildren(theParentName, composite, newNode, theParent, theComponentNumbers, theTerserPath);
532 
533 			newNode = (TreeNodeType) insertOrReplaceWithExisting(theTreeParent, theIndex, newNode);
534 
535 		} else {
536 
537 			Primitive primitive = (Primitive) theType;
538 			TreeNodeType newNode;
539 			if (primitive instanceof ConformancePrimitive) {
540 				newNode = new TreeNodePrimitiveConf(theParentName, (ConformancePrimitive) primitive, theName, theRepNum, theRepeating, theRequired, theParent, theComponentNumbers, theTerserPath);
541 			} else {
542 				newNode = new TreeNodePrimitive(theParentName, primitive, theName, theRepNum, theRepeating, theRequired, theParent, theComponentNumbers, theTerserPath);
543 			}
544 
545 			addChidrenExtra(theParentName, primitive, newNode, theParent, theComponentNumbers, theTerserPath, theComponentNumbers.size(), 0);
546 
547 			newNode = (TreeNodeType) insertOrReplaceWithExisting(theTreeParent, theIndex, newNode);
548 
549 		}
550 
551 		return theIndex + 1;
552 	}
553 
554 
555 	public void collapseAll() {
556 		AbstractLayoutCache layout = ((OutlineModel) getModel()).getLayout();
557 		for (int i = 0; i < layout.getRowCount(); i++) {
558 			TreePath path = layout.getPathForRow(i);
559 			collapsePath(path);
560 		}
561 	}
562 
563 	public void destroy() {
564 		removeMessageListeners();
565 
566 		myTop.destroy();
567 		myUpdaterThread.stopThread();
568 	}
569 
570 	private void doSynchronizeTreeWithHighlitedPath() {
571 		String highlitedPath = myMessages.getHighlitedPath();
572 		if (highlitedPath == null) {
573 			return;
574 		}
575 
576 		final AbstractLayoutCache layout = ((OutlineModel) getModel()).getLayout();
577 		int lastSegmentRow = -1;
578 		int currentSegmentRow = -1;
579 		int currentSelectedRow = -1;
580 		int currentMessageIndex = -1;
581 		for (int row = 0; row < layout.getRowCount(); row++) {
582 
583 			TreePath path = layout.getPathForRow(row);
584 			Object component = path.getLastPathComponent();
585 			if (component instanceof TreeNodeMessage) {
586 				currentMessageIndex = ((TreeNodeMessage) component).getMessageIndex();
587 				if (highlitedPath.startsWith(currentMessageIndex + "/")) {
588 					expandPath(path);
589 				} else {
590 					// collapsePath(path);
591 				}
592 				continue;
593 			}
594 
595 			if (component instanceof TreeNodeUnknown) {
596 				continue;
597 			}
598 
599 			if (component instanceof TreeNodeSegment) {
600 				lastSegmentRow = row;
601 			}
602 
603 			TreeNodeBase node = (TreeNodeBase) component;
604 
605 			String terserPath = (currentMessageIndex) + node.getTerserPath();
606 			if (highlitedPath != null && highlitedPath.startsWith(terserPath) && !highlitedPath.startsWith(terserPath + "(")) {
607 				expandPath(path);
608 				if (highlitedPath.equals(terserPath)) {
609 					currentSelectedRow = row;
610 					getSelectionModel().setSelectionInterval(row, row);
611 					currentSegmentRow = lastSegmentRow;
612 				}
613 			} else {
614 				// collapsePath(path);
615 			}
616 
617 		}
618 
619 		// Adjust the tree scrollpane's scroll position so that the newly
620 		// selected row is visible
621 		if (currentSegmentRow != -1 && currentSelectedRow != -1 && !myRespondingToManualRangeChange) {
622 			JViewport viewPort = (JViewport) getParent();
623 			final JScrollPane scrollPane = (JScrollPane) viewPort.getParent();
624 
625 			int tableHeaderHeight = getTableHeader().getHeight();
626 
627 			int numRowsVisible = ((scrollPane.getHeight() - tableHeaderHeight) / layout.getRowHeight()) - 1;
628 			int segmentDelta = currentSelectedRow - currentSegmentRow;
629 			if (segmentDelta > numRowsVisible) {
630 				currentSegmentRow = currentSegmentRow + (segmentDelta - numRowsVisible);
631 			}
632 
633 			final int scrollToRow = currentSegmentRow;
634 			SwingUtilities.invokeLater(new Runnable() {
635 				public void run() {
636 					scrollPane.getVerticalScrollBar().setValue(layout.getRowHeight() * scrollToRow);
637 				}
638 			});
639 
640 		}
641 	}
642 
643 	public void expandAll() {
644 		AbstractLayoutCache layout = ((OutlineModel) getModel()).getLayout();
645 		for (int i = 0; i < layout.getRowCount(); i++) {
646 			TreePath path = layout.getPathForRow(i);
647 			expandPath(path);
648 		}
649 	}
650 
651 	private void expandPaths(Set<String> theOpenPaths, String theSelectedPath) {
652 		AbstractLayoutCache layout = ((OutlineModel) getModel()).getLayout();
653 		int messageIndex = -1;
654 		for (int i = 0; i < layout.getRowCount(); i++) {
655 			TreePath path = layout.getPathForRow(i);
656 
657 			Object baseObj = path.getLastPathComponent();
658 			String pathString = null;
659 			if (baseObj instanceof TreeNodeMessage || baseObj instanceof TreeNodeUnknown) {
660 				messageIndex++;
661 				pathString = Integer.toString(messageIndex);
662 			} else if (baseObj instanceof TreeNodeBase) {
663 				pathString = (Integer.toString(messageIndex) + ((TreeNodeBase) baseObj).getTerserPath());
664 			}
665 
666 			if (pathString != null) {
667 				if (theOpenPaths.contains(pathString)) {
668 					expandPath(path);
669 				} else {
670 					collapsePath(path);
671 				}
672 				if (pathString.equals(theSelectedPath)) {
673 					getSelectionModel().setSelectionInterval(i, i);
674 				}
675 			}
676 
677 		}
678 
679 	}
680 
681 	private Set<String> getOpenPaths() {
682 		Set<String> retVal = new HashSet<String>();
683 
684 		TableModel model = getModel();
685 		AbstractLayoutCache layout = ((OutlineModel) model).getLayout();
686 		int messageIndex = -1;
687 		for (int i = 0; i < layout.getRowCount(); i++) {
688 			TreePath path = layout.getPathForRow(i);
689 
690 			Object baseObj = path.getLastPathComponent();
691 			if (baseObj instanceof TreeNodeMessage || baseObj instanceof TreeNodeUnknown) {
692 
693 				messageIndex++;
694 
695 				if (layout.getExpandedState(path)) {
696 					retVal.add(Integer.toString(messageIndex));
697 				}
698 
699 			} else {
700 
701 				baseObj = path.getPathComponent(path.getPathCount() - 2);
702 				if (baseObj instanceof TreeNodeBase) {
703 					retVal.add(Integer.toString(messageIndex) + ((TreeNodeBase) baseObj).getTerserPath());
704 				}
705 			}
706 
707 		}
708 
709 		return retVal;
710 	}
711 
712 	private String getPathAtRow(int theRowIndex) {
713 
714 		AbstractLayoutCache layout = ((OutlineModel) getModel()).getLayout();
715 		int messageIndex = -1;
716 		for (int i = 0; i < layout.getRowCount(); i++) {
717 			TreePath path = layout.getPathForRow(i);
718 
719 			Object baseObj = path.getLastPathComponent();
720 			if (baseObj instanceof TreeNodeMessage || baseObj instanceof TreeNodeUnknown) {
721 
722 				messageIndex++;
723 				if (i == theRowIndex) {
724 					return Integer.toString(messageIndex);
725 				}
726 
727 			} else {
728 
729 				if (i == theRowIndex && baseObj instanceof TreeNodeBase) {
730 					return (Integer.toString(messageIndex) + ((TreeNodeBase) baseObj).getTerserPath());
731 				}
732 
733 			}
734 
735 		}
736 
737 		return null;
738 	}
739 
740 	private ShowEnum getShowMode() {
741 		if (myUnitTestShowMode != null) {
742 			return myUnitTestShowMode;
743 		}
744 		ShowEnum showMode = myMessages != null ? myMessages.getEditorShowMode() : ShowEnum.POPULATED;
745 		return showMode;
746 	}
747 
748 	private void handleKeyPress(KeyEvent theE) {
749 		int row = getSelectedRow();
750 		if (row == -1) {
751 			return;
752 		}
753 
754 		if (theE.getKeyCode() == KeyEvent.VK_ENTER || theE.getKeyCode() == KeyEvent.VK_F2) {
755 			AbstractLayoutCache layout = ((OutlineModel) getModel()).getLayout();
756 			TreePath path = layout.getPathForRow(row);
757 			TreeNodeBase baseObj = (TreeNodeBase) path.getLastPathComponent();
758 			if (myTableModel.isCellEditable(baseObj, TreeRowModel.COL_VALUE)) {
759 				editCellAt(row, TreeRowModel.COL_VALUE + 1);
760 				theE.consume();
761 			}
762 		}
763 	}
764 
765 	private void handleNewSelectedIndex(int theNewIndex) {
766 		if (mySelectionHandlingDisabled) {
767 			return;
768 		}
769 		ourLog.info("New selection index: " + theNewIndex);
770 
771 		if (myCurrentlyEditing) {
772 			ourLog.info("Not responding to new selection index because we are marked as editing right now");
773 			return;
774 		}
775 
776 		AbstractLayoutCache layout = ((OutlineModel) getModel()).getLayout();
777 		TreePath path = layout.getPathForRow(theNewIndex);
778 
779 		DefaultMutableTreeNode lead = (DefaultMutableTreeNode) path.getLastPathComponent();
780 		if (lead instanceof TreeNodeSegment) {
781 			TreeNodeSegment segmentNode = (TreeNodeSegment) lead;
782 			myMessages.setHighlitedRangeBasedOnSegment(segmentNode.getSegment());
783 		} else if (lead instanceof TreeNodeGroup) {
784 			TreeNodeGroup type = (TreeNodeGroup) lead;
785 			try {
786 				List<Segment> segments = type.getSegments();
787 				myMessages.setHighlitedRangeBasedOnSegment(segments.toArray(new Segment[segments.size()]));
788 			} catch (HL7Exception e) {
789 				e.printStackTrace();
790 			}
791 		} else if (lead instanceof TreeNodeType) {
792 			TreeNodeType type = (TreeNodeType) lead;
793 			myMessages.setHighlitedRangeBasedOnField(type.getSegmentAndComponentPath());
794 		} else {
795 			myMessages.clearHighlight();
796 		}
797 
798 	}
799 
800 	private void removeMessageListeners() {
801 		if (myMessages != null) {
802 			myMessages.removePropertyChangeListener(Hl7V2MessageCollection.PROP_HIGHLITED_PATH, myHighlitedPathListener);
803 			myMessages.removePropertyChangeListener(Hl7V2MessageCollection.PARSED_MESSAGES_PROPERTY, myParsedMessagesListener);
804 			myMessages.removePropertyChangeListener(Hl7V2MessageCollection.PROP_VALIDATIONCONTEXT_OR_PROFILE, myValidationContextListener);
805 			myMessages.removePropertyChangeListener(Hl7V2MessageCollection.PROP_ENCODING, myMessageEncodingListener);
806 		}
807 	}
808 
809 	public void scheduleNewValidationPass() {
810 		myUpdaterThread.scheduleUpdate();
811 	}
812 
813 	/**
814 	 * {@inheritDoc}
815 	 */
816 	@Override
817 	public void setEditingRow(int theARow) {
818 		if (theARow == -1) {
819 			myCurrentlyEditing = false;
820 		} else {
821 			ourLog.info("Beginning editing row " + theARow);
822 			myCurrentlyEditing = true;
823 		}
824 		super.setEditingRow(theARow);
825 	}
826 
827 	public void setEditorShowModeAndUpdateAccordingly(ShowEnum theValue) {
828 		if (myMessages != null && theValue != myMessages.getEditorShowMode()) {
829 			myMessages.setEditorShowMode(theValue);
830 			myUpdaterThread.scheduleUpdateNow();
831 		}
832 	}
833 
834 	/**
835 	 * Updates the panel with a new Message.
836 	 */
837 	public void setMessage(Hl7V2MessageCollection theMessage) {
838 
839 		removeMessageListeners();
840 
841 		myMessages = theMessage;
842 
843 		myMessages.addPropertyChangeListener(Hl7V2MessageCollection.PROP_HIGHLITED_PATH, myHighlitedPathListener);
844 		myMessages.addPropertyChangeListener(Hl7V2MessageCollection.PARSED_MESSAGES_PROPERTY, myParsedMessagesListener);
845 		myMessages.addPropertyChangeListener(Hl7V2MessageCollection.PROP_VALIDATIONCONTEXT_OR_PROFILE, myValidationContextListener);
846 		myMessages.addPropertyChangeListener(Hl7V2MessageCollection.PROP_ENCODING, myMessageEncodingListener);
847 
848 		myTop = new TreeNodeRoot();
849 		myTreeModel = new DefaultTreeModel(myTop, false);
850 
851 		myTableModel = new TreeRowModel(myTreeModel);
852 		OutlineModel outlineModel = DefaultOutlineModel.createOutlineModel(myTreeModel, myTableModel);
853 		setModel(outlineModel);
854 
855 		setRootVisible(false);
856 
857 		setDefaultRenderer(NodeValidationFailure.class, new ValidationTreeCellRenderer());
858 
859 		// Volumn index is off by one because of the tree
860 		getColumnModel().getColumn(TreeRowModel.COL_VALUE + 1).setCellRenderer(new ValueCellRenderer(this));
861 
862 		updateUI();
863 
864 		myUpdaterThread.scheduleUpdateNow();
865 
866 		SwingUtilities.invokeLater(new Runnable() {
867 
868 			public void run() {
869 				int width = getWidth() - 140;
870 
871 				getColumnModel().getColumn(0).setPreferredWidth(width / 2);
872 
873 				// Min
874 				getColumnModel().getColumn(1).setPreferredWidth(35);
875 				getColumnModel().getColumn(1).setMinWidth(35);
876 				getColumnModel().getColumn(1).setMaxWidth(35);
877 
878 				// Max
879 				getColumnModel().getColumn(2).setPreferredWidth(35);
880 				getColumnModel().getColumn(2).setMinWidth(35);
881 				getColumnModel().getColumn(2).setMaxWidth(35);
882 
883 				// Length
884 				getColumnModel().getColumn(3).setPreferredWidth(50);
885 				getColumnModel().getColumn(3).setMinWidth(50);
886 				getColumnModel().getColumn(3).setMaxWidth(50);
887 
888 				// Validated
889 				getColumnModel().getColumn(4).setPreferredWidth(20);
890 				getColumnModel().getColumn(4).setMinWidth(20);
891 				getColumnModel().getColumn(4).setMaxWidth(20);
892 
893 				getColumnModel().getColumn(5).setPreferredWidth(width / 2);
894 			}
895 		});
896 
897 	}
898 
899 	void setMessageForUnitTest(Hl7V2MessageCollection theMessageModel) {
900 		myMessages = theMessageModel;
901 	}
902 
903 	void setRuntimeProfileValidator(DefaultValidator theRuntimeProfileValidator) {
904 		myRuntimeProfileValidator = theRuntimeProfileValidator;
905 	}
906 
907 	public void setUnitTestShowMode(ShowEnum theUnitTestShowMode) {
908 		myUnitTestShowMode = theUnitTestShowMode;
909 		myUpdaterThread.scheduleUpdateNow();
910 	}
911 
912 	/**
913 	 * @param theWorkingListener
914 	 *            the workingListener to set
915 	 */
916 	public void setWorkingListener(IWorkingListener theWorkingListener) {
917 		myWorkingListener = theWorkingListener;
918 	}
919 
920 	private void synchronizeTreeWithHighlitedPath() {
921 		try {
922 			mySelectionHandlingDisabled = true;
923 			doSynchronizeTreeWithHighlitedPath();
924 		} finally {
925 			mySelectionHandlingDisabled = false;
926 		}
927 	}
928 
929 	private String xmlEncode(String theValue) {
930 		if (theValue == null) {
931 			return null;
932 		}
933 
934 		StringBuilder b = new StringBuilder();
935 		for (int i = 0; i < theValue.length(); i++) {
936 			char nextChar = theValue.charAt(i);
937 			switch (nextChar) {
938 			case '\r':
939 			case '\n':
940 				b.append("<br>");
941 				break;
942 			case ' ':
943 				b.append("&nbsp;");
944 				break;
945 			case '&':
946 				b.append("&amp;");
947 				break;
948 			case '<':
949 				b.append("&lt;");
950 				break;
951 			case '>':
952 				b.append("&gt;");
953 				break;
954 			default:
955 				b.append(nextChar);
956 			}
957 		}
958 		return b.toString();
959 	}
960 
961 	private static TreeNodeBase insertOrReplaceWithExisting(final TreeNodeBase theTreeParent, final int theIndex, final TreeNodeBase theNewNode) throws InterruptedException, InvocationTargetException {
962 
963 		if (theTreeParent.getChildCount() <= theIndex) {
964 			EventQueue.invokeAndWait(new Runnable() {
965 				public void run() {
966 					theTreeParent.insert(theNewNode, theIndex);
967 				}
968 			});
969 			return theNewNode;
970 		}
971 
972 		// if (theTreeParent.getChildAt(theIndex).equals(theNewNode)) {
973 		// return (TreeNodeBase) theTreeParent.getChildAt(theIndex);
974 		// }
975 
976 		while (theTreeParent.getChildCount() > (theIndex)) {
977 			TreeNode node = theTreeParent.getChildAt(theIndex);
978 			if (node instanceof IDestroyable) {
979 				((IDestroyable) node).destroy();
980 			}
981 
982 			EventQueue.invokeAndWait(new Runnable() {
983 				public void run() {
984 					theTreeParent.remove(theIndex);
985 				}
986 			});
987 			if (theTreeParent.getChildCount() > (theIndex) && theTreeParent.getChildAt(theIndex).equals(theNewNode)) {
988 				return (TreeNodeBase) theTreeParent.getChildAt(theIndex);
989 			}
990 		}
991 
992 		EventQueue.invokeAndWait(new Runnable() {
993 			public void run() {
994 				theTreeParent.insert(theNewNode, theIndex);
995 			}
996 		});
997 
998 		return theNewNode;
999 	}
1000 
1001 	/**
1002 	 * Left pads a string representation of the integer to make it 4 digits long
1003 	 */
1004 	public static String toHl7Table(int theTable) {
1005 		return StringUtils.leftPad(Integer.toString(theTable), 4, '0');
1006 	}
1007 
1008 	public interface IWorkingListener {
1009 		void finishedWorking(String theStatus);
1010 
1011 		void startedWorking();
1012 
1013 	}
1014 
1015 	/**
1016 	 * Not sure if it's a bug in outline or what, but this seems to be the only
1017 	 * way to reliably know what row is selected
1018 	 */
1019 	public class MySelectionModel extends DefaultListSelectionModel {
1020 
1021 		public void addSelectionInterval(int theIndex0, int theIndex1) {
1022 			// ignore
1023 		}
1024 
1025 		public void removeSelectionInterval(int theIndex0, int theIndex1) {
1026 			// ignore
1027 		}
1028 
1029 		public void setSelectionInterval(int theIndex0, int theIndex1) {
1030 			myRespondingToManualRangeChange = true;
1031 			try {
1032 				handleNewSelectedIndex(theIndex0);
1033 				super.setSelectionInterval(theIndex0, theIndex1);
1034 			} finally {
1035 				myRespondingToManualRangeChange = false;
1036 			}
1037 		}
1038 
1039 	}
1040 
1041 	private class NodeValidationFailure {
1042 		private Icon myIcon;
1043 		private String myMessage;
1044 
1045 		public NodeValidationFailure(Icon theIcon, String theMessage) {
1046 			super();
1047 			myIcon = theIcon;
1048 			myMessage = theMessage;
1049 		}
1050 
1051 		/**
1052 		 * @return the icon
1053 		 */
1054 		public Icon getIcon() {
1055 			return myIcon;
1056 		}
1057 
1058 		/**
1059 		 * @return the message
1060 		 */
1061 		public String getMessage() {
1062 			return myMessage;
1063 		}
1064 	}
1065 
1066 	public abstract class TreeNodeBase extends DefaultMutableTreeNode {
1067 		protected static final String COLOR_REPNUM = "#808000";
1068 
1069 		private Boolean myContainError;
1070 		private String myErrorDescription;
1071 		private Boolean myHasContent;
1072 		private final String myName;
1073 		private final Boolean myRepeating;
1074 		private final int myRepNum;
1075 		private final Boolean myRequired;
1076 		private final String myTerserPath;
1077 		private List<HL7Exception> myValidationExceptions = new ArrayList<HL7Exception>();
1078 
1079 		public TreeNodeBase(Object theStructure) {
1080 			super(theStructure);
1081 			assert theStructure != null || this instanceof TreeNodeRoot;
1082 
1083 			myName = null;
1084 			myTerserPath = null;
1085 			myRepNum = 0;
1086 			myRepeating = null;
1087 			myRequired = null;
1088 		}
1089 
1090 		public TreeNodeBase(Object theStructure, String theName, int theRepNum, Boolean theRepeating, Boolean theRequired, String theTerserPath) {
1091 			super(theStructure);
1092 			assert theStructure != null;
1093 
1094 			myName = theName;
1095 			myRepNum = theRepNum;
1096 			myRepeating = theRepeating;
1097 			myRequired = theRequired;
1098 			myTerserPath = theTerserPath;
1099 		}
1100 
1101 		public void addValidationExceptions(List<HL7Exception> theProblems) {
1102 			addValidationExceptions(theProblems.toArray(new HL7Exception[theProblems.size()]));
1103 		}
1104 
1105 		public void addValidationExceptions(HL7Exception... theExceptions) {
1106 			for (HL7Exception hl7Exception : theExceptions) {
1107 				myValidationExceptions.add(hl7Exception);
1108 			}
1109 		}
1110 
1111 		public int countExceptions() {
1112 			int retVal = 0;
1113 
1114 			for (int i = 0; i < getChildCount(); i++) {
1115 				TreeNodeBase next = (TreeNodeBase) getChildAt(i);
1116 				retVal += next.countExceptions();
1117 			}
1118 
1119 			retVal += myValidationExceptions.size();
1120 			return retVal;
1121 		}
1122 
1123 		/**
1124 		 * Subclasses may override if validation is possible
1125 		 */
1126 		public void doValidate() {
1127 			// nothing
1128 		}
1129 
1130 		@Override
1131 		public boolean equals(Object theObj) {
1132 			if (theObj == null || !getClass().equals(theObj.getClass())) {
1133 				return false;
1134 			}
1135 			return ((TreeNodeBase) theObj).getUserObject() == getUserObject();
1136 		}
1137 
1138 		public String getDisplayName() {
1139 			return null;
1140 		}
1141 
1142 		/**
1143 		 * @return the errorDescription
1144 		 */
1145 		public String getErrorDescription() {
1146 			if (myErrorDescription == null && myValidationExceptions.size() > 0) {
1147 				StringBuilder b = new StringBuilder();
1148 				b.append("<html><ul>");
1149 				for (HL7Exception next : myValidationExceptions) {
1150 					b.append("<li>");
1151 					b.append(next.getMessage());
1152 				}
1153 				b.append("</ul></html>");
1154 
1155 				myErrorDescription = b.toString();
1156 			}
1157 			return myErrorDescription;
1158 		}
1159 
1160 		public Integer getMaxLength() {
1161 			return null;
1162 		}
1163 
1164 		public Short getMaxReps() {
1165 			if (isRepeating() == null) {
1166 				return null;
1167 			} else if (isRepeating()) {
1168 				return -1;
1169 			} else {
1170 				return 1;
1171 			}
1172 		}
1173 
1174 		public Short getMinReps() {
1175 			if (isRequired() == null) {
1176 				return null;
1177 			} else if (isRequired()) {
1178 				return 1;
1179 			} else {
1180 				return 0;
1181 			}
1182 		}
1183 
1184 		/**
1185 		 * @return the groupName
1186 		 */
1187 		public String getName() {
1188 			return myName;
1189 		}
1190 
1191 		public StringBuilder getNodeText() {
1192 			StringBuilder b = new StringBuilder();
1193 
1194 			b.append("<font color=\"" + getNodeTextColor() + "\">");
1195 			b.append(myName);
1196 			b.append("</font>");
1197 
1198 			if (myRepeating != null && myRepeating && (myShowRep0 || getRepNum() > 0)) {
1199 				b.append("<font color=\"" + COLOR_REPNUM + "\">");
1200 				b.append(" (rep");
1201 				if (myRepNum > 0) {
1202 					b.append(' ');
1203 					b.append(myRepNum + 1);
1204 				}
1205 				b.append(")");
1206 				b.append("</font>");
1207 			}
1208 
1209 			if (StringUtils.isNotBlank(getDisplayName())) {
1210 				b.append("<font color=\"#00A000\">");
1211 				b.append(" - ");
1212 				b.append(getDisplayName());
1213 				b.append("</font>");
1214 			}
1215 
1216 			return b;
1217 		}
1218 
1219 		public String getNodeTextColor() {
1220 			return "#000000";
1221 		}
1222 
1223 		public String getPipeEncodedValue() {
1224 			return null;
1225 		}
1226 
1227 		/**
1228 		 * @return the repNum
1229 		 */
1230 		public int getRepNum() {
1231 			return myRepNum;
1232 		}
1233 
1234 		/**
1235 		 * @return the terserPath
1236 		 */
1237 		public String getTerserPath() {
1238 			return myTerserPath;
1239 		}
1240 
1241 		@Override
1242 		public int hashCode() {
1243 			Object object = getUserObject();
1244 			if (object != null) {
1245 				return object.hashCode();
1246 			} else {
1247 				return super.hashCode();
1248 			}
1249 		}
1250 
1251 		/**
1252 		 * @return the containError
1253 		 */
1254 		public boolean isContainError() {
1255 			if (myContainError == null) {
1256 				if (isHasContent() == false) {
1257 					myContainError = false;
1258 				} else if (getErrorDescription() != null) {
1259 					myContainError = true;
1260 				} else {
1261 					for (int i = 0; i < getChildCount(); i++) {
1262 						TreeNodeBase nextChild = (TreeNodeBase) getChildAt(i);
1263 						if (nextChild.isHasContent() && nextChild.isContainError()) {
1264 							myContainError = true;
1265 							break;
1266 						}
1267 					}
1268 					if (myContainError == null) {
1269 						myContainError = false;
1270 					}
1271 				}
1272 			}
1273 			return myContainError;
1274 		}
1275 
1276 		public Boolean isHasContent() {
1277 			if (myHasContent == null) {
1278 				for (int i = 0; i < getChildCount(); i++) {
1279 					TreeNodeBase next = (TreeNodeBase) getChildAt(i);
1280 					if (next.isHasContent() == Boolean.TRUE) {
1281 						myHasContent = Boolean.TRUE;
1282 						break;
1283 					}
1284 				}
1285 
1286 				if (myHasContent == null) {
1287 					myHasContent = Boolean.FALSE;
1288 				}
1289 			}
1290 			return myHasContent;
1291 		}
1292 
1293 		/**
1294 		 * @return the repeating
1295 		 */
1296 		public Boolean isRepeating() {
1297 			return myRepeating;
1298 		}
1299 
1300 		/**
1301 		 * @return the required
1302 		 */
1303 		public Boolean isRequired() {
1304 			return myRequired;
1305 		}
1306 
1307 		/**
1308 		 * Subclasses may override
1309 		 */
1310 		protected boolean isSupported() {
1311 			return true;
1312 		}
1313 
1314 		/**
1315 		 * @param theErrorDescription
1316 		 *            the errorDescription to set
1317 		 */
1318 		public void setErrorDescription(String theErrorDescription) {
1319 			myErrorDescription = theErrorDescription;
1320 		}
1321 
1322 		public final void validate() throws InterruptedException, InvocationTargetException {
1323 			for (int i = 0; i < getChildCount(); i++) {
1324 				TreeNodeBase next = (TreeNodeBase) getChildAt(i);
1325 
1326 				if (next.isHasContent()) {
1327 					next.validate();
1328 				}
1329 
1330 				ShowEnum showMode = myMessages.getEditorShowMode();
1331 				if ((next.getErrorDescription() == null && showMode == ShowEnum.ERROR) || (next.isHasContent() == false && showMode == ShowEnum.POPULATED) || (next.isSupported() == false && next.getErrorDescription() == null && showMode == ShowEnum.SUPPORTED)) {
1332 					final int index = i;
1333 					EventQueue.invokeAndWait(new Runnable() {
1334 						public void run() {
1335 							remove(index);
1336 						}
1337 					});
1338 					i--;
1339 					continue;
1340 				}
1341 
1342 			}
1343 
1344 			doValidate();
1345 		}
1346 	}
1347 
1348 	public class TreeNodeComment extends TreeNodeBase implements IDestroyable {
1349 		private PropertyChangeListener myListener;
1350 		private Comment myMessage;
1351 
1352 		public TreeNodeComment(Comment theMessage) {
1353 			super(theMessage);
1354 
1355 			myMessage = theMessage;
1356 
1357 			myListener = new PropertyChangeListener() {
1358 
1359 				public void propertyChange(PropertyChangeEvent theEvt) {
1360 					myTreeModel.nodeStructureChanged(myTop);
1361 				}
1362 			};
1363 			myMessage.addPropertyChangeListener(UnknownMessage.PARSED_MESSAGE_PROPERTY, myListener);
1364 
1365 		}
1366 
1367 		/**
1368 		 * {@inheritDoc}
1369 		 */
1370 		public void destroy() {
1371 			myMessage.addPropertyChangeListener(UnknownMessage.PARSED_MESSAGE_PROPERTY, myListener);
1372 		}
1373 
1374 		@Override
1375 		public void doValidate() {
1376 			// nothing
1377 		}
1378 
1379 		@Override
1380 		public StringBuilder getNodeText() {
1381 			StringBuilder retVal = new StringBuilder();
1382 			retVal.append("<html><font color=\"#00FF00\">");
1383 			retVal.append(myMessage.getSourceMessage());
1384 			retVal.append("</font></html>");
1385 			return retVal;
1386 		}
1387 
1388 	}
1389 
1390 	public class TreeNodeCompositeConf extends TreeNodeType {
1391 
1392 		public TreeNodeCompositeConf(String theParentName, Type theComposite, String theGroupName, int theRepNum, boolean theRepeating, boolean theRequired, Segment theParent, List<Integer> theComponentPath, String theTerserPath) {
1393 			super(theParentName, theComposite, theGroupName, theRepNum, theRepeating, theRequired, theParent, theComponentPath, theTerserPath);
1394 		}
1395 
1396 		@Override
1397 		public void doValidate() {
1398 			EncodingCharacters enc;
1399 			try {
1400 				enc = EncodingCharacters.getInstance(getComposite().getMessage());
1401 			} catch (HL7Exception e) {
1402 				ourLog.error("Could not get encoding chars", e);
1403 				enc = new EncodingCharacters('|', null);
1404 			}
1405 
1406 			String encoded = PipeParser.encode(getComposite(), enc);
1407 			List<HL7Exception> problems = myRuntimeProfileValidator.testType(getComposite(), getComposite().getConfDefinition(), encoded, "");
1408 			addValidationExceptions(problems);
1409 		}
1410 
1411 		public ConformanceComposite getComposite() {
1412 			return (ConformanceComposite) super.getUserObject();
1413 		}
1414 
1415 		/**
1416 		 * {@inheritDoc}
1417 		 */
1418 		@Override
1419 		protected String getDataTypeDescription() {
1420 			String retVal = getComposite().getConfDefinition().getDatatype();
1421 			return retVal;
1422 		}
1423 
1424 		@Override
1425 		public String getDisplayName() {
1426 			return getComposite().getConfDefinition().getName();
1427 		}
1428 
1429 		@Override
1430 		public Integer getMaxLength() {
1431 			return (int) getComposite().getConfDefinition().getLength();
1432 		}
1433 
1434 		protected boolean isSupported() {
1435 			return !"X".equals(getComposite().getConfDefinition().getUsage());
1436 		}
1437 
1438 	}
1439 
1440 	public class TreeNodeGroup extends TreeNodeGroupBase {
1441 
1442 		public TreeNodeGroup(Group theGroup, String theGroupName, int theRepNum, boolean theRepeating, boolean theRequired, String theTerserPath) {
1443 			super(theGroup, theGroupName, theRepNum, theRepeating, theRequired, theTerserPath);
1444 		}
1445 
1446 		private void addToSegList(List<Segment> retVal, Group group) throws HL7Exception {
1447 			for (String next : group.getNames()) {
1448 				for (Structure nextStructure : group.getAll(next)) {
1449 					if (nextStructure instanceof Segment) {
1450 						retVal.add((Segment) nextStructure);
1451 					} else {
1452 						addToSegList(retVal, (Group) nextStructure);
1453 					}
1454 				}
1455 			}
1456 		}
1457 
1458 		public Group getGroup() {
1459 			return (Group) getUserObject();
1460 		}
1461 
1462 		@Override
1463 		public String getNodeTextColor() {
1464 			return "#404000";
1465 		}
1466 
1467 		public List<Segment> getSegments() throws HL7Exception {
1468 
1469 			List<Segment> retVal = new ArrayList<Segment>();
1470 
1471 			Group group = getGroup();
1472 			addToSegList(retVal, group);
1473 
1474 			return retVal;
1475 		}
1476 
1477 	}
1478 
1479 	public class TreeNodeGroupBase extends TreeNodeBase {
1480 		public TreeNodeGroupBase(Group theGroup, String theGroupName, int theRepNum, boolean theRepeating, boolean theRequired, String theTerserPath) {
1481 			super(theGroup, theGroupName, theRepNum, theRepeating, theRequired, theTerserPath);
1482 		}
1483 
1484 		public TreeNodeGroupBase(Hl7V2MessageBase theMessage) {
1485 			super(theMessage);
1486 		}
1487 
1488 		public int countPopulatedSegments() {
1489 			int retVal = 0;
1490 
1491 			for (int i = 0; i < getChildCount(); i++) {
1492 				TreeNode nextStructure = getChildAt(i);
1493 				if (nextStructure instanceof TreeNodeSegment) {
1494 					if (((TreeNodeSegment) nextStructure).isHasContent()) {
1495 						retVal++;
1496 					}
1497 				} else if (nextStructure instanceof TreeNodeGroup) {
1498 					retVal += ((TreeNodeGroup) nextStructure).countPopulatedSegments();
1499 				}
1500 			}
1501 
1502 			return retVal;
1503 		}
1504 
1505 	}
1506 
1507 	public class TreeNodeGroupConf extends TreeNodeGroup {
1508 
1509 		public TreeNodeGroupConf(ConformanceGroup theGroup, String theGroupName, int theRepNum, boolean theRepeating, boolean theRequired, String theTerserPath) {
1510 			super(theGroup, theGroupName, theRepNum, theRepeating, theRequired, theTerserPath);
1511 		}
1512 
1513 		@Override
1514 		public void doValidate() {
1515 			try {
1516 				List<HL7Exception> problems = myRuntimeProfileValidator.testGroup(getGroup(), getGroup().getConfDefinition(), "");
1517 				addValidationExceptions(problems);
1518 			} catch (ProfileException e) {
1519 				addValidationExceptions(new HL7Exception(e));
1520 			}
1521 		}
1522 
1523 		/**
1524 		 * {@inheritDoc}
1525 		 */
1526 		@Override
1527 		public String getDisplayName() {
1528 			return getGroup().getConfDefinition().getLongName();
1529 		}
1530 
1531 		public ConformanceGroup getGroup() {
1532 			return (ConformanceGroup) super.getGroup();
1533 		}
1534 
1535 		protected boolean isSupported() {
1536 			return !"X".equals(getGroup().getConfDefinition().getUsage());
1537 		}
1538 
1539 	}
1540 
1541 	public class TreeNodeMessage extends TreeNodeGroupBase implements IDestroyable {
1542 		private int myMessageIndex;
1543 		private PropertyChangeListener myParsedMessageListener;
1544 
1545 		public TreeNodeMessage(int theMessageIndex, final Hl7V2MessageBase theMessage) {
1546 			super(theMessage);
1547 
1548 			myMessageIndex = theMessageIndex;
1549 
1550 			myParsedMessageListener = new PropertyChangeListener() {
1551 
1552 				public void propertyChange(PropertyChangeEvent theEvt) {
1553 					myUpdaterThread.scheduleUpdate();
1554 				}
1555 			};
1556 			theMessage.addPropertyChangeListener(Hl7V2MessageBase.PARSED_MESSAGE_PROPERTY, myParsedMessageListener);
1557 
1558 		}
1559 
1560 		public void destroy() {
1561 			((Hl7V2MessageBase) getUserObject()).removePropertyChangeListener(Hl7V2MessageBase.PARSED_MESSAGE_PROPERTY, myParsedMessageListener);
1562 		}
1563 
1564 		public Hl7V2MessageBase getMessage() {
1565 			return (Hl7V2MessageBase) getUserObject();
1566 		}
1567 
1568 		/**
1569 		 * @return the messageIndex
1570 		 */
1571 		public int getMessageIndex() {
1572 			return myMessageIndex;
1573 		}
1574 
1575 		public Message getParsedMessage() {
1576 			return getMessage().getParsedMessage();
1577 		}
1578 	}
1579 
1580 	public class TreeNodeMessageConf extends TreeNodeMessage {
1581 
1582 		public TreeNodeMessageConf(int theIndex, Hl7V2MessageBase theMessage) {
1583 			super(theIndex, theMessage);
1584 		}
1585 
1586 		@Override
1587 		public void doValidate() {
1588 			try {
1589 				HL7Exception[] problems = myRuntimeProfileValidator.validate(getParsedMessage(), getParsedMessage().getConfDefinition());
1590 				addValidationExceptions(problems);
1591 			} catch (ProfileException e) {
1592 				addValidationExceptions(new HL7Exception(e));
1593 			} catch (HL7Exception e) {
1594 				addValidationExceptions(e);
1595 			}
1596 		}
1597 
1598 		/**
1599 		 * {@inheritDoc}
1600 		 */
1601 		@Override
1602 		public String getDisplayName() {
1603 			return getParsedMessage().getConfDefinition().getDescription();
1604 		}
1605 
1606 		/**
1607 		 * {@inheritDoc}
1608 		 */
1609 		@Override
1610 		public ConformanceMessage getParsedMessage() {
1611 			return (ConformanceMessage) super.getParsedMessage();
1612 		}
1613 
1614 	}
1615 
1616 	public class TreeNodePrimitive extends TreeNodeType {
1617 
1618 		public TreeNodePrimitive(String theParentName, Primitive theGroup, String theGroupName, int theRepNum, boolean theRepeating, boolean theRequired, Segment theParent, List<Integer> theComponentPath, String theTerserPath) {
1619 			super(theParentName, theGroup, theGroupName, theRepNum, theRepeating, theRequired, theParent, theComponentPath, theTerserPath);
1620 		}
1621 
1622 		/**
1623 		 * {@inheritDoc}
1624 		 */
1625 		@Override
1626 		public void doValidate() {
1627 			super.doValidate();
1628 
1629 			Primitive primitive = getPrimitive();
1630 			if (myMessages != null) {
1631 				if (myMessages.getRuntimeProfile() != null) {
1632 
1633 					// If we're using a conformance profile, also
1634 					// use datatype validation as well
1635 					String version = primitive.getMessage().getVersion();
1636 					String typeName = primitive.getName();
1637 					Collection<PrimitiveTypeRule> rules = ourDefaultValidation.getPrimitiveRules(version, typeName, primitive);
1638 					for (PrimitiveTypeRule rule : rules) {
1639 						if (!rule.test(primitive.getValue())) {
1640 							addValidationExceptions(new HL7Exception(rule.getDescription()));
1641 						}
1642 					}
1643 
1644 				} else if (myMessages.getValidationContext() != null) {
1645 
1646 					String version = primitive.getMessage().getVersion();
1647 					String type = primitive.getName();
1648 					Collection<PrimitiveTypeRule> rules = myMessages.getValidationContext().getPrimitiveRules(version, type, primitive);
1649 					for (PrimitiveTypeRule primitiveTypeRule : rules) {
1650 						boolean test = primitiveTypeRule.test(primitive.getValue());
1651 						if (!test) {
1652 							// setErrorDescription(primitiveTypeRule.getDescription());
1653 							addValidationExceptions(new HL7Exception(primitiveTypeRule.getDescription()));
1654 						}
1655 					}
1656 
1657 				}
1658 
1659 			}
1660 
1661 		}
1662 
1663 		@Override
1664 		protected String getDataTypeDescription() {
1665 			Primitive primitive = getPrimitive();
1666 			if (primitive instanceof ID) {
1667 				return super.getDataTypeDescription() + TBL + toHl7Table(((ID) primitive).getTable());
1668 			}
1669 			if (primitive instanceof IS) {
1670 				return super.getDataTypeDescription() + TBL + toHl7Table(((IS) primitive).getTable());
1671 			}
1672 			return super.getDataTypeDescription();
1673 		}
1674 
1675 		public Primitive getPrimitive() {
1676 			return (Primitive) getUserObject();
1677 		}
1678 
1679 		protected String getTable() {
1680 			Primitive prim = getPrimitive();
1681 			String namespace = TABLE_NAMESPACE_HL7;
1682 			int retVal = 0;
1683 			if (prim instanceof IS) {
1684 				retVal = (((IS) prim).getTable());
1685 			} else if (prim instanceof ID) {
1686 				retVal = (((ID) prim).getTable());
1687 			}
1688 			return retVal > 0 ? namespace + toHl7Table(retVal) : null;
1689 		}
1690 
1691 		@Override
1692 		public Boolean isHasContent() {
1693 			Primitive p = (Primitive) getUserObject();
1694 			String value = p.getValue();
1695 			boolean retVal = value != null && value.length() > 0;
1696 			if (retVal) {
1697 				return retVal;
1698 			}
1699 
1700 			for (int i = 0; i < p.getExtraComponents().numComponents(); i++) {
1701 				try {
1702 					value = p.getExtraComponents().getComponent(i).encode();
1703 				} catch (HL7Exception e) {
1704 					return false;
1705 				}
1706 				retVal = value != null && value.length() > 0;
1707 				if (retVal) {
1708 					return retVal;
1709 				}
1710 			}
1711 
1712 			return false;
1713 		}
1714 
1715 	}
1716 
1717 	public class TreeNodePrimitiveConf extends TreeNodePrimitive {
1718 
1719 		public TreeNodePrimitiveConf(String theParentName, ConformancePrimitive thePrimitive, String theGroupName, int theRepNum, boolean theRepeating, boolean theRequired, Segment theParent, List<Integer> theComponentPath, String theTerserPath) {
1720 			super(theParentName, thePrimitive, theGroupName, theRepNum, theRepeating, theRequired, theParent, theComponentPath, theTerserPath);
1721 
1722 		}
1723 
1724 		@Override
1725 		public void doValidate() {
1726 			ConformancePrimitive primitive = getPrimitive();
1727 			String tp = getTerserPath();
1728 
1729 			EncodingCharacters enc;
1730 			try {
1731 				enc = EncodingCharacters.getInstance(primitive.getMessage());
1732 			} catch (HL7Exception e) {
1733 				ourLog.error("Could not get encoding chars", e);
1734 				enc = new EncodingCharacters('|', null);
1735 			}
1736 
1737 			String encoded = PipeParser.encode(primitive, enc);
1738 			if (tp.endsWith("MSH-1") || tp.endsWith("MSH-2")) {
1739 				encoded = primitive.getValue();
1740 			}
1741 
1742 			List<HL7Exception> problems = myRuntimeProfileValidator.testType(getPrimitive(), getPrimitive().getConfDefinition(), encoded, "");
1743 			addValidationExceptions(problems);
1744 
1745 			if (myMessages.getRuntimeProfile() != null) {
1746 				String table = getTable();
1747 				if (table != null) {
1748 
1749 					ConformanceMessage msg = getPrimitive().getMessage();
1750 					String tablesId = msg.getTablesId();
1751 					if (StringUtils.isNotBlank(tablesId)) {
1752 						TableFile tableFile = myController.getTableFileList().getTableFile(tablesId);
1753 						if (tableFile != null) {
1754 							if (tableFile.knowsCodes(table)) {
1755 								String value = StringUtils.defaultString(primitive.getValue());
1756 								if (!tableFile.isValidCode(table, value)) {
1757 									addValidationExceptions(new HL7Exception("Not a valid value in table '" + table + "': " + value));
1758 								}
1759 							}
1760 						}
1761 					}
1762 				}
1763 			}
1764 
1765 			super.doValidate();
1766 		}
1767 
1768 		/**
1769 		 * {@inheritDoc}
1770 		 */
1771 		@Override
1772 		protected String getDataTypeDescription() {
1773 			String retVal = getPrimitive().getConfDefinition().getDatatype();
1774 			String table = getPrimitive().getConfDefinition().getTable();
1775 			if (StringUtils.isNotBlank(table)) {
1776 				return retVal + TBL + table;
1777 			} else {
1778 				return retVal;
1779 			}
1780 		}
1781 
1782 		@Override
1783 		public String getDisplayName() {
1784 			return getPrimitive().getConfDefinition().getName();
1785 		}
1786 
1787 		@Override
1788 		public Integer getMaxLength() {
1789 			return (int) getPrimitive().getConfDefinition().getLength();
1790 		}
1791 
1792 		public ConformancePrimitive getPrimitive() {
1793 			return (ConformancePrimitive) super.getPrimitive();
1794 		}
1795 
1796 		@Override
1797 		protected String getTable() {
1798 			String retVal = getPrimitive().getConfDefinition().getTable();
1799 			if (StringUtils.isBlank(retVal)) {
1800 				return null;
1801 			} else {
1802 				if (AbstractNumericPrimitive.isInteger(retVal)) {
1803 					return TABLE_NAMESPACE_HL7 + retVal;
1804 				} else {
1805 					return retVal;
1806 				}
1807 			}
1808 		}
1809 
1810 		protected boolean isSupported() {
1811 			return !"X".equals(getPrimitive().getConfDefinition().getUsage());
1812 		}
1813 
1814 	}
1815 
1816 	public class TreeNodeRoot extends TreeNodeBase implements IDestroyable {
1817 
1818 		public TreeNodeRoot() {
1819 			super(null);
1820 		}
1821 
1822 		public int countMessages() {
1823 			int retVal = 0;
1824 			for (int i = 0; i < getChildCount(); i++) {
1825 				if (getChildAt(i) instanceof TreeNodeMessage) {
1826 					retVal++;
1827 				}
1828 			}
1829 			return retVal;
1830 		}
1831 
1832 		public void destroy() {
1833 			for (int i = 0; i < getChildCount(); i++) {
1834 				TreeNode next = getChildAt(i);
1835 				if (next instanceof IDestroyable) {
1836 					((IDestroyable) next).destroy();
1837 				}
1838 			}
1839 		}
1840 
1841 	}
1842 
1843 	public class TreeNodeSegment extends TreeNodeBase {
1844 		public TreeNodeSegment(Segment theSegment, String theGroupName, int theRepNum, boolean theRepeating, boolean theRequired, String theTerserPath) {
1845 			super(theSegment, theGroupName, theRepNum, theRepeating, theRequired, theTerserPath);
1846 
1847 			Validate.notNull(theTerserPath);
1848 			Validate.isTrue(theTerserPath.startsWith("/"));
1849 		}
1850 
1851 		/**
1852 		 * {@inheritDoc}
1853 		 */
1854 		@Override
1855 		public StringBuilder getNodeText() {
1856 			StringBuilder retVal = super.getNodeText();
1857 
1858 			if (isNonStandard()) {
1859 				retVal.append("<font color=\"#A0A000\">");
1860 				retVal.append(" (non standard)");
1861 				retVal.append("</font>");
1862 			}
1863 
1864 			return retVal;
1865 		}
1866 
1867 		@Override
1868 		public String getNodeTextColor() {
1869 			return "#006000";
1870 		}
1871 
1872 		/**
1873 		 * {@inheritDoc}
1874 		 */
1875 		@Override
1876 		public String getPipeEncodedValue() {
1877 			EncodingCharacters enc;
1878 			try {
1879 				enc = EncodingCharacters.getInstance(getSegment().getMessage());
1880 			} catch (HL7Exception e) {
1881 				ourLog.error("Could not get encoding chars", e);
1882 				enc = new EncodingCharacters('|', null);
1883 			}
1884 			return PipeParser.encode(getSegment(), enc);
1885 		}
1886 
1887 		public Segment getSegment() {
1888 			return (Segment) getUserObject();
1889 		}
1890 
1891 		@Override
1892 		public Boolean isHasContent() {
1893 			return getPipeEncodedValue().length() > 3;
1894 		}
1895 
1896 		public boolean isNonStandard() {
1897 			AbstractGroup parent = (AbstractGroup) getSegment().getParent();
1898 			return parent.getNonStandardNames().contains(getSegment().getName());
1899 		}
1900 
1901 	}
1902 
1903 	public class TreeNodeSegmentConf extends TreeNodeSegment {
1904 		public TreeNodeSegmentConf(ConformanceSegment theSegment, String theGroupName, int theRepNum, boolean theRepeating, boolean theRequired, String theTerserPath) {
1905 			super(theSegment, theGroupName, theRepNum, theRepeating, theRequired, theTerserPath);
1906 		}
1907 
1908 		@Override
1909 		public void doValidate() {
1910 			try {
1911 				List<HL7Exception> problems = myRuntimeProfileValidator.testSegment(getSegment(), getSegment().getConfDefinition(), "");
1912 				addValidationExceptions(problems);
1913 			} catch (ProfileException e) {
1914 				addValidationExceptions(new HL7Exception(e));
1915 			}
1916 		}
1917 
1918 		/**
1919 		 * {@inheritDoc}
1920 		 */
1921 		@Override
1922 		public String getDisplayName() {
1923 			return getSegment().getConfDefinition().getLongName();
1924 		}
1925 
1926 		/**
1927 		 * {@inheritDoc}
1928 		 */
1929 		@Override
1930 		public Short getMaxReps() {
1931 			return getSegment().getMaxReps();
1932 		}
1933 
1934 		/**
1935 		 * {@inheritDoc}
1936 		 */
1937 		@Override
1938 		public Short getMinReps() {
1939 			return getSegment().getMinReps();
1940 		}
1941 
1942 		public ConformanceSegment getSegment() {
1943 			return (ConformanceSegment) getUserObject();
1944 		}
1945 
1946 		protected boolean isSupported() {
1947 			return !"X".equals(getSegment().getConfDefinition().getUsage());
1948 		}
1949 	}
1950 
1951 	public class TreeNodeType extends TreeNodeBase {
1952 		private ArrayList<Integer> myComponentPath;
1953 		private String myParentName;
1954 		private Segment mySegment;
1955 
1956 		public TreeNodeType(String theParentName, Type theGroup, String theGroupName, int theRepNum, boolean theRepeating, boolean theRequired, Segment theParent, List<Integer> theComponentPath, String theTerserPath) {
1957 			super(theGroup, theGroupName, theRepNum, theRepeating, theRequired, theTerserPath);
1958 
1959 			Validate.notNull(theParent);
1960 			Validate.notNull(theComponentPath);
1961 			Validate.notEmpty(theComponentPath);
1962 
1963 			mySegment = theParent;
1964 			myParentName = theParentName;
1965 			myComponentPath = new ArrayList<Integer>(theComponentPath);
1966 		}
1967 
1968 		protected String getDataTypeDescription() {
1969 			return getType().getClass().getSimpleName();
1970 		}
1971 
1972 		public StringBuilder getNodeText() {
1973 			StringBuilder b = new StringBuilder();
1974 
1975 			b.append(myParentName);
1976 			// b.append(" ");
1977 			// b.append(getName());
1978 
1979 			if (isRepeating() && (myShowRep0 || getRepNum() > 0)) {
1980 				b.append("<font color=\"" + COLOR_REPNUM + "\">");
1981 				b.append(" (rep");
1982 				if (getRepNum() > 0) {
1983 					b.append(' ');
1984 					b.append(getRepNum() + 1);
1985 				}
1986 				b.append(")");
1987 				b.append("</font>");
1988 			}
1989 
1990 			b.append("<font color=\"#00A000\">");
1991 			if (StringUtils.isNotBlank(getDisplayName())) {
1992 				b.append(" - ");
1993 				b.append(getDisplayName());
1994 				b.append(" ");
1995 			}
1996 			b.append(" (");
1997 			b.append(getDataTypeDescription());
1998 			b.append(")");
1999 			b.append("</font>");
2000 
2001 			return b;
2002 		}
2003 
2004 		/**
2005 		 * {@inheritDoc}
2006 		 */
2007 		@Override
2008 		public String getPipeEncodedValue() {
2009 			// Don't encode MSH-1 or 2 since this will escape them
2010 			if (isMsh1orMsh2()) {
2011 				return ((Primitive) getType()).getValue();
2012 			}
2013 
2014 			EncodingCharacters enc;
2015 			try {
2016 				enc = EncodingCharacters.getInstance(getType().getMessage());
2017 			} catch (HL7Exception e) {
2018 				ourLog.error("Could not get encoding chars", e);
2019 				enc = new EncodingCharacters('|', null);
2020 			}
2021 			return PipeParser.encode(getType(), enc);
2022 		}
2023 
2024 		public SegmentAndComponentPath getSegmentAndComponentPath() {
2025 			return new SegmentAndComponentPath(mySegment, myComponentPath, getRepNum() + 1);
2026 		}
2027 
2028 		public Type getType() {
2029 			return (Type) getUserObject();
2030 		}
2031 
2032 		public boolean isMsh1orMsh2() {
2033 			return "MSH-1".equals(myParentName) || "MSH-2".equals(myParentName);
2034 		}
2035 
2036 	}
2037 
2038 	public class TreeNodeUnknown extends TreeNodeBase implements IDestroyable {
2039 		private PropertyChangeListener myListener;
2040 		private UnknownMessage myMessage;
2041 
2042 		public TreeNodeUnknown(UnknownMessage theMessage) {
2043 			super(theMessage);
2044 
2045 			myMessage = theMessage;
2046 
2047 			myListener = new PropertyChangeListener() {
2048 
2049 				public void propertyChange(PropertyChangeEvent theEvt) {
2050 					myTreeModel.nodeStructureChanged(myTop);
2051 				}
2052 			};
2053 			myMessage.addPropertyChangeListener(UnknownMessage.PARSED_MESSAGE_PROPERTY, myListener);
2054 
2055 		}
2056 
2057 		/**
2058 		 * {@inheritDoc}
2059 		 */
2060 		public void destroy() {
2061 			myMessage.addPropertyChangeListener(UnknownMessage.PARSED_MESSAGE_PROPERTY, myListener);
2062 		}
2063 
2064 		@Override
2065 		public StringBuilder getNodeText() {
2066 			StringBuilder retVal = new StringBuilder();
2067 			retVal.append("<html><font color=\"#FF0000\">Unknown</font><font color=\"#A0A0A0\"> ");
2068 
2069 			int countLines = StringUtil.countLines(myMessage.getSourceMessage().trim());
2070 			retVal.append(countLines);
2071 			retVal.append(" Line");
2072 
2073 			if (countLines != 1) {
2074 				retVal.append("s");
2075 			}
2076 
2077 			retVal.append("</font></html>");
2078 			return retVal;
2079 		}
2080 
2081 	}
2082 
2083 	public class TreeNodeUnknownLine extends TreeNodeBase {
2084 		public TreeNodeUnknownLine(Object theLine) {
2085 			super(theLine);
2086 		}
2087 
2088 		@Override
2089 		public StringBuilder getNodeText() {
2090 			StringBuilder retVal = new StringBuilder();
2091 			retVal.append("<html><font color=\"#4040A0\">");
2092 
2093 			Object object = getUserObject();
2094 			if (object != null) {
2095 				retVal.append(xmlEncode(object.toString()));
2096 			}
2097 
2098 			retVal.append("</font></html>");
2099 			return retVal;
2100 		}
2101 
2102 	}
2103 
2104 	private static class TreeRenderDataProvider implements RenderDataProvider {
2105 
2106 		private static final Color ourFgEmpty = new Color(0.5f, 0.5f, 0.5f);
2107 		private static final Color ourFgGroup = new Color(0.4f, 0.4f, 0.0f);
2108 		private static final Color ourFgNormal = new Color(0.0f, 0.0f, 0.0f);
2109 		private static final Color ourFgSegment = new Color(0.0f, 0.0f, 0.0f);
2110 
2111 		public Color getBackground(Object theArg0) {
2112 			return Color.white;
2113 		}
2114 
2115 		public String getDisplayName(Object theObject) {
2116 			if (theObject instanceof TreeNodeMessage) {
2117 
2118 				TreeNodeMessage tnm = (TreeNodeMessage) theObject;
2119 				return (tnm.getMessage().getMessageDescription());
2120 
2121 			} else if (theObject instanceof TreeNodeBase) {
2122 
2123 				TreeNodeBase base = (TreeNodeBase) theObject;
2124 				return (base.getNodeText().toString());
2125 
2126 			} else {
2127 
2128 				return "Unknown: " + theObject.getClass().getName();
2129 
2130 			}
2131 		}
2132 
2133 		public Color getForeground(Object theArg0) {
2134 			if (theArg0 instanceof TreeNodeBase) {
2135 				if (Boolean.FALSE == ((TreeNodeBase) theArg0).isHasContent()) {
2136 					return ourFgEmpty;
2137 				}
2138 			}
2139 			if (theArg0 instanceof TreeNodeGroup) {
2140 				return ourFgGroup;
2141 			}
2142 			if (theArg0 instanceof TreeNodeSegment) {
2143 				return ourFgSegment;
2144 			}
2145 			return ourFgNormal;
2146 		}
2147 
2148 		public Icon getIcon(Object theArg0) {
2149 			if (theArg0 instanceof TreeNodeGroup) {
2150 				return ImageFactory.getTreeBundle();
2151 			} else if (theArg0 instanceof TreeNodeSegment) {
2152 				return ImageFactory.getTreeLeaf();
2153 			} else {
2154 				return new ImageIcon();
2155 			}
2156 		}
2157 
2158 		public String getTooltipText(Object theArg0) {
2159 			return null;
2160 		}
2161 
2162 		public boolean isHtmlDisplayName(Object theArg0) {
2163 			return true;
2164 		}
2165 
2166 	}
2167 
2168 	private class TreeRowModel implements RowModel {
2169 
2170 		private static final int COL_LENGTH = 2;
2171 		private static final int COL_MAX = 1;
2172 		private static final int COL_MIN = 0;
2173 		private static final int COL_VALIDATED = 3;
2174 		private static final int COL_VALUE = 4;
2175 
2176 		private static final int NUM_COLS = 5;
2177 
2178 		public TreeRowModel(DefaultTreeModel theModel) {
2179 			myTreeModel = theModel;
2180 		}
2181 
2182 		public Class<?> getColumnClass(int theArg0) {
2183 			if (theArg0 == COL_VALIDATED) {
2184 				return NodeValidationFailure.class;
2185 			}
2186 			return String.class;
2187 		}
2188 
2189 		public int getColumnCount() {
2190 			return NUM_COLS;
2191 		}
2192 
2193 		public String getColumnName(int theArg0) {
2194 			switch (theArg0) {
2195 			case COL_MIN:
2196 				return "Min";
2197 			case COL_MAX:
2198 				return "Max";
2199 			case COL_VALIDATED:
2200 				return "";
2201 			case COL_LENGTH:
2202 				return "Length";
2203 			case COL_VALUE:
2204 			default:
2205 				return "Value (Click to Edit)";
2206 			}
2207 		}
2208 
2209 		public Object getValueFor(Object theObject, int theCol) {
2210 			TreeNodeBase obj = (TreeNodeBase) theObject;
2211 			switch (theCol) {
2212 			case COL_VALUE: {
2213 				return obj;
2214 			}
2215 			case COL_MIN:
2216 				if (obj.getMinReps() == null) {
2217 					return null;
2218 				} else {
2219 					return obj.getMinReps();
2220 				}
2221 			case COL_MAX:
2222 				if (obj.getMaxReps() == null) {
2223 					return null;
2224 				} else if (obj.getMaxReps() == -1) {
2225 					return "*";
2226 				} else {
2227 					return obj.getMaxReps();
2228 				}
2229 			case COL_LENGTH:
2230 				return obj.getMaxLength();
2231 			case COL_VALIDATED:
2232 				// if (myMessages.getValidationContext() == null) {
2233 				// return new NodeValidationFailure(new ImageIcon(), "");
2234 				// } else
2235 				if (obj.isContainError()) {
2236 					if (obj.getErrorDescription() != null) {
2237 						return new NodeValidationFailure(ImageFactory.getValFailed(), obj.getErrorDescription());
2238 					} else {
2239 						return new NodeValidationFailure(ImageFactory.getValFailedChild(), "Child element has validation failure");
2240 					}
2241 				} else {
2242 					return new NodeValidationFailure(new ImageIcon(), "");
2243 				}
2244 			default:
2245 				return "";
2246 			}
2247 		}
2248 
2249 		public boolean isCellEditable(Object theValue, int theColumn) {
2250 			if (theColumn == COL_VALUE) {
2251 				if (theValue instanceof TreeNodeSegment) {
2252 					return true;
2253 				}
2254 				if (theValue instanceof TreeNodeType) {
2255 					return true;
2256 				}
2257 			}
2258 			return false;
2259 		}
2260 
2261 		private void parse(Segment theSegment, String theNewValue) throws HL7Exception {
2262 			EncodingCharacters enc;
2263 			try {
2264 				enc = EncodingCharacters.getInstance(theSegment.getMessage());
2265 			} catch (HL7Exception e) {
2266 				ourLog.error("Could not get encoding chars", e);
2267 				enc = new EncodingCharacters('|', null);
2268 			}
2269 
2270 			myPipeParser.parse(theSegment, theNewValue, enc);
2271 		}
2272 
2273 		private void parse(Type theType, String theNewValue) throws HL7Exception {
2274 			EncodingCharacters enc;
2275 			try {
2276 				enc = EncodingCharacters.getInstance(theType.getMessage());
2277 			} catch (HL7Exception e) {
2278 				ourLog.error("Could not get encoding chars", e);
2279 				enc = new EncodingCharacters('|', null);
2280 			}
2281 
2282 			theType.clear();
2283 			myPipeParser.parse(theType, theNewValue, enc);
2284 
2285 		}
2286 
2287 		public void setValueFor(Object theObject, int theCol, Object theNewValue) {
2288 			if (theCol != COL_VALUE) {
2289 				return;
2290 			}
2291 
2292 			Message msg = null;
2293 			String newValue = (String) theNewValue;
2294 			if (theObject instanceof TreeNodeSegment) {
2295 				try {
2296 					Segment segment = ((TreeNodeSegment) theObject).getSegment();
2297 					parse(segment, newValue);
2298 					msg = segment.getMessage();
2299 				} catch (HL7Exception e) {
2300 					ourLog.error("Could not set value: " + theNewValue, e);
2301 					return;
2302 				}
2303 			} else if (theObject instanceof TreeNodeType) {
2304 				try {
2305 					TreeNodeType type = ((TreeNodeType) theObject);
2306 
2307 					if (!myController.validateNewValue(type.getTerserPath(), newValue)) {
2308 						return;
2309 					}
2310 
2311 					if (type.isMsh1orMsh2()) {
2312 						((Primitive) type.getType()).setValue(newValue);
2313 					} else {
2314 						parse(type.getType(), newValue);
2315 					}
2316 
2317 					msg = type.getType().getMessage();
2318 
2319 				} catch (HL7Exception e) {
2320 					ourLog.error("Could not set value: " + theNewValue, e);
2321 					return;
2322 				}
2323 			} else {
2324 				return;
2325 			}
2326 
2327 			if (msg != null) {
2328 
2329 				TreeNodeBase base = (TreeNodeBase) theObject;
2330 				while (!(base instanceof TreeNodeMessage)) {
2331 					base = (TreeNodeBase) base.getParent();
2332 				}
2333 
2334 				int messageIndex = ((TreeNodeMessage) base).getMessageIndex();
2335 				myMessages.updateSourceMessageBasedOnParsedMessage(messageIndex, msg);
2336 			}
2337 
2338 			/*
2339 			 * After making a change, the underlying collection may reparse the
2340 			 * entire message which will invalidate the references to various
2341 			 * structures held by my tree
2342 			 */
2343 			myUpdaterThread.scheduleUpdateNow();
2344 
2345 		}
2346 
2347 	}
2348 
2349 	private class UpdaterThread extends Thread {
2350 		private long myNextUpdate = 0;
2351 
2352 		@Override
2353 		public void run() {
2354 
2355 			while (myNextUpdate > -1) {
2356 
2357 				try {
2358 					long sleepTime = myNextUpdate > 0 ? myNextUpdate - System.currentTimeMillis() : 5000;
2359 					sleepTime = Math.max(0, sleepTime);
2360 					sleepTime = Math.min(5000, sleepTime);
2361 
2362 					try {
2363 						Thread.sleep(sleepTime);
2364 					} catch (InterruptedException e) {
2365 						// ignore
2366 					}
2367 
2368 					if (myNextUpdate > 0 && myNextUpdate <= System.currentTimeMillis()) {
2369 						ourLog.info("Running an update of the Message Tree");
2370 
2371 						addChildren();
2372 
2373 						int messages = myTop.countMessages();
2374 
2375 						final StringBuilder b = new StringBuilder();
2376 						b.append(messages > 0 ? messages : "No");
2377 						b.append(" message");
2378 						b.append(messages != 1 ? "s" : "");
2379 						if (myMessages.isValidating()) {
2380 							b.append(", ");
2381 							int countExceptions = myTop.countExceptions();
2382 							b.append(countExceptions > 0 ? countExceptions : "No");
2383 							b.append(" problem");
2384 							b.append(countExceptions != 1 ? "s" : "");
2385 						}
2386 
2387 						if (myWorkingListener != null) {
2388 							EventQueue.invokeAndWait(new Runnable() {
2389 								public void run() {
2390 									myWorkingListener.finishedWorking(b.toString());
2391 								}
2392 							});
2393 						}
2394 
2395 						myNextUpdate = 0;
2396 					}
2397 
2398 				} catch (InterruptedException e) {
2399 
2400 					// We can ignore these, they happen if the message is
2401 					// updated by
2402 					// the UI during a middle of an update loop
2403 
2404 				} catch (Throwable e) {
2405 
2406 					ourLog.info("Exception caught during update loop", e);
2407 					try {
2408 						Thread.sleep(1000);
2409 					} catch (InterruptedException e2) {
2410 						// ignore
2411 					}
2412 
2413 				}
2414 
2415 			} // while
2416 
2417 			ourLog.info("Message Tree updater shutting down");
2418 
2419 		}
2420 
2421 		public void scheduleUpdate() {
2422 			myNextUpdate = System.currentTimeMillis() + 2000;
2423 			interrupt();
2424 
2425 			if (myWorkingListener != null) {
2426 				EventQueue.invokeLater(new Runnable() {
2427 					public void run() {
2428 						myWorkingListener.startedWorking();
2429 					}
2430 				});
2431 			}
2432 
2433 		}
2434 
2435 		public void scheduleUpdateNow() {
2436 			myNextUpdate = System.currentTimeMillis();
2437 			interrupt();
2438 
2439 			if (myWorkingListener != null) {
2440 				EventQueue.invokeLater(new Runnable() {
2441 					public void run() {
2442 						myWorkingListener.startedWorking();
2443 					}
2444 				});
2445 			}
2446 
2447 		}
2448 
2449 		public void stopThread() {
2450 			myNextUpdate = -1;
2451 			interrupt();
2452 		}
2453 
2454 	}
2455 
2456 	private class ValidationTreeCellRenderer extends DefaultTableCellRenderer {
2457 
2458 		@Override
2459 		public Component getTableCellRendererComponent(JTable theTable, Object theValue, boolean theIsSelected, boolean theHasFocus, int theRow, int theColumn) {
2460 			Component tableCellRendererComponent = super.getTableCellRendererComponent(theTable, theValue, theIsSelected, theHasFocus, theRow, theColumn);
2461 
2462 			NodeValidationFailure vf = (NodeValidationFailure) theValue;
2463 			setIcon(vf.getIcon());
2464 			setToolTipText(vf.getMessage());
2465 			setText("");
2466 
2467 			return tableCellRendererComponent;
2468 		}
2469 
2470 	}
2471 
2472 }