package ca.uhn.hl7v2.util; import ca.uhn.hl7v2.parser.*; import ca.uhn.hl7v2.HL7Exception; import ca.uhn.hl7v2.model.Message; import java.io.*; import java.util.ArrayList; /** * Tests correctness of message parsing by testing equivalence of re-encoded * form with original. * @author Bryan Tripp */ public class ParseTester { private static GenericParser parser = new GenericParser(); private BufferedReader source; private String context; /** Creates a new instance of ParseTester */ public ParseTester() { } /** * Checks whether the given message parses correctly with a GenericParser. * Failure indicates that the parsed and re-encoded message is semantically * different than the original, or that the message could not be parsed. This * may stem from an error in the parser, or from an error in the message. This * may also arise from unexpected message components (e.g. Z-segments) although * in future HAPI versions these will be parsed as well. * @param message an XML or ER7 encoded message string * @return null if it parses correctly, an HL7Exception otherwise */ public static HL7Exception parsesCorrectly(String context, String message) { HL7Exception problem = null; try { Message m = parser.parse(message); String encoding = parser.getEncoding(message); String result = parser.encode(m, encoding); if (!EncodedMessageComparator.equivalent(message, result)) { problem = new HL7Exception(context + ": Original differs semantically from parsed/encoded message.\r\n-----Original:------------\r\n" + message + " \r\n------ Parsed/Encoded: ----------\r\n" + result + " \r\n-----Original Standardized: ---------\r\n" + EncodedMessageComparator.standardize(message) + " \r\n---------------------\r\n"); } } catch (Exception e) { problem = new HL7Exception(context + ": " + e.getMessage() + " in message: \r\n-------------\r\n" + message + "\r\n-------------");; } return problem; } /** * Sets the source of message data (messages must be delimited by blank lines) */ public void setSource(Reader source) { this.source = new BufferedReader(new CommentFilterReader(source)); } /** * Sets a description of the context of the messages (e.g. file name) that can be * reported within error messages. */ public void setContext(String description) { this.context = description; } /** * Sets the source reader to point to the given file, and tests * all the messages therein (if a directory, processes all contained * files recursively). */ public HL7Exception[] testAll(File source) throws IOException { ArrayList list = new ArrayList(); System.out.println("Testing " + source.getPath()); if (source.isDirectory()) { File[] contents = source.listFiles(); for (int i = 0; i < contents.length; i++) { HL7Exception[] exceptions = testAll(contents[i]); for (int j = 0; j < exceptions.length; j++) { list.add(exceptions[j]); } } } else if (source.isFile()) { FileReader in = new FileReader(source); setSource(in); setContext(source.getAbsolutePath()); HL7Exception[] exceptions = testAll(); for (int i = 0; i < exceptions.length; i++) { list.add(exceptions[i]); } } else { System.out.println("Warning: " + source.getPath() + " is not a normal file"); } return (HL7Exception[]) list.toArray(new HL7Exception[0]); } /** * Tests all remaining messages available from the currrent source. */ public HL7Exception[] testAll() throws IOException { ArrayList list = new ArrayList(); String message = null; while ((message = getNextMessage()).length() > 0) { HL7Exception e = parsesCorrectly(this.context, message); if (e != null) list.add(e); } return (HL7Exception[]) list.toArray(new HL7Exception[0]); } /** * Retrieves the next message (setSource() must be called first). The next message * is interpreted as everything up to the next blank line, not including * C or C++ style comments (or blank lines themselves). An empty string * indicates that there are no more messages. */ public String getNextMessage() throws IOException { if (this.source == null) throw new IOException("Message source is null -- call setSource() first"); StringBuffer message = new StringBuffer(); boolean started = false; //got at least one non-blank line boolean finished = false; //got a blank line after started, or end of stream while (!finished) { String line = this.source.readLine(); if (line == null || (started && line.trim().length() == 0)) { finished = true; } else { if (line.trim().length() > 0) { started = true; message.append(line); message.append("\r"); } } } if (message.toString().trim().length() == 0) { return ""; } else { return message.toString(); // can't trim by default (will omit final end-segment) } } /** * Command line tool for testing messages in files. */ public static void main(String args[]) { if (args.length != 1 || args[0].equalsIgnoreCase("-?") || args[0].equalsIgnoreCase("-h") || args[0].equalsIgnoreCase("-help")) { System.out.println("USAGE:"); System.out.println(" ParseTester "); System.out.println(); System.out.println(" must be either a file containing HL7 messages or a directory containing such files"); System.out.println(); System.out.println("Notes:"); System.out.println(" - Messages can be XML or ER7 encoded. "); System.out.println(" - If there are multiple messages in a file they must be delimited by blank lines"); System.out.println(" - C and C++ style comments are skipped"); } else { try { System.out.println("Testing ... "); File source = new File(args[0]); ParseTester tester = new ParseTester(); HL7Exception[] exceptions = tester.testAll(source); if (exceptions.length > 0) System.out.println("Parsing problems with tested messages: "); for (int i = 0; i < exceptions.length; i++) { System.out.println("PROBLEM #" + (i+1)); System.out.println(exceptions[i].getMessage()); } } catch (IOException e) { System.out.println("Testing failed to complete because of a problem reading source file(s) ... \r\n"); e.printStackTrace(); } } } /** * Removes C and C++ style comments from a reader stream. C style comments are * distinguished from URL protocol delimiters by the preceding colon in the * latter. */ public static class CommentFilterReader extends PushbackReader { private final char[] startCPPComment = {'/', '*'}; private final char[] endCPPComment = {'*', '/'}; private final char[] startCComment = {'/', '/'}; private final char[] endCComment = {'\n'}; private final char[] protocolDelim = {':', '/', '/'}; public CommentFilterReader(Reader in) { super(in, 5); } /** * Returns the next character, not including comments. */ public int read() throws IOException { if (atSequence(protocolDelim)) { //proceed normally } else if (atSequence(startCPPComment)) { //skip() doesn't seem to work for some reason while (!atSequence(endCPPComment)) super.read(); for (int i = 0; i < endCPPComment.length; i++) super.read(); } else if (atSequence(startCComment)) { while (!atSequence(endCComment)) super.read(); for (int i = 0; i < endCComment.length; i++) super.read(); } return super.read(); } public int read(char[] cbuf, int off, int len) throws IOException { int i = -1; boolean done = false; while (++i < len) { int next = read(); if (next == 65535 || next == -1) { //Pushback causes -1 to convert to 65535 done = true; break; } cbuf[off + i] = (char) next; } if (i == 0 && done) i = -1; return i; } /** * Tests incoming data for match with char sequence, resets reader when done. */ private boolean atSequence(char[] sequence) throws IOException { boolean result = true; int i = -1; int[] data = new int[sequence.length]; while (++i < sequence.length && result == true) { data[i] = super.read(); if ((char) data[i] != sequence[i]) result = false; //includes case where end of stream reached } for (int j = i-1; j >= 0; j--) { this.unread(data[j]); } return result; } } }