001 package ca.uhn.hl7v2.util; 002 003 import ca.uhn.hl7v2.parser.*; 004 import ca.uhn.hl7v2.HL7Exception; 005 import ca.uhn.hl7v2.model.Message; 006 import java.io.*; 007 import java.util.ArrayList; 008 009 /** 010 * Tests correctness of message parsing by testing equivalence of re-encoded 011 * form with original. 012 * @author Bryan Tripp 013 */ 014 public class ParseTester { 015 016 private static GenericParser parser = new GenericParser(); 017 private BufferedReader source; 018 private String context; 019 020 /** Creates a new instance of ParseTester */ 021 public ParseTester() { 022 } 023 024 /** 025 * Checks whether the given message parses correctly with a GenericParser. 026 * Failure indicates that the parsed and re-encoded message is semantically 027 * different than the original, or that the message could not be parsed. This 028 * may stem from an error in the parser, or from an error in the message. This 029 * may also arise from unexpected message components (e.g. Z-segments) although 030 * in future HAPI versions these will be parsed as well. 031 * @param message an XML or ER7 encoded message string 032 * @return null if it parses correctly, an HL7Exception otherwise 033 */ 034 public static HL7Exception parsesCorrectly(String context, String message) { 035 HL7Exception problem = null; 036 try { 037 Message m = parser.parse(message); 038 String encoding = parser.getEncoding(message); 039 String result = parser.encode(m, encoding); 040 if (!EncodedMessageComparator.equivalent(message, result)) { 041 problem = new HL7Exception(context + ": Original differs semantically from parsed/encoded message.\r\n-----Original:------------\r\n" 042 + message + " \r\n------ Parsed/Encoded: ----------\r\n" + result + " \r\n-----Original Standardized: ---------\r\n" 043 + EncodedMessageComparator.standardize(message) + " \r\n---------------------\r\n"); 044 } 045 } catch (Exception e) { 046 problem = new HL7Exception(context + ": " + e.getMessage() + " in message: \r\n-------------\r\n" + message + "\r\n-------------");; 047 } 048 return problem; 049 } 050 051 /** 052 * Sets the source of message data (messages must be delimited by blank lines) 053 */ 054 public void setSource(Reader source) { 055 this.source = new BufferedReader(new CommentFilterReader(source)); 056 } 057 058 /** 059 * Sets a description of the context of the messages (e.g. file name) that can be 060 * reported within error messages. 061 */ 062 public void setContext(String description) { 063 this.context = description; 064 } 065 066 /** 067 * Sets the source reader to point to the given file, and tests 068 * all the messages therein (if a directory, processes all contained 069 * files recursively). 070 */ 071 public HL7Exception[] testAll(File source) throws IOException { 072 ArrayList list = new ArrayList(); 073 System.out.println("Testing " + source.getPath()); 074 if (source.isDirectory()) { 075 File[] contents = source.listFiles(); 076 for (int i = 0; i < contents.length; i++) { 077 HL7Exception[] exceptions = testAll(contents[i]); 078 for (int j = 0; j < exceptions.length; j++) { 079 list.add(exceptions[j]); 080 } 081 } 082 } else if (source.isFile()) { 083 FileReader in = new FileReader(source); 084 setSource(in); 085 setContext(source.getAbsolutePath()); 086 HL7Exception[] exceptions = testAll(); 087 for (int i = 0; i < exceptions.length; i++) { 088 list.add(exceptions[i]); 089 } 090 } else { 091 System.out.println("Warning: " + source.getPath() + " is not a normal file"); 092 } 093 return (HL7Exception[]) list.toArray(new HL7Exception[0]); 094 } 095 096 /** 097 * Tests all remaining messages available from the currrent source. 098 */ 099 public HL7Exception[] testAll() throws IOException { 100 ArrayList list = new ArrayList(); 101 102 String message = null; 103 while ((message = getNextMessage()).length() > 0) { 104 HL7Exception e = parsesCorrectly(this.context, message); 105 if (e != null) list.add(e); 106 } 107 108 return (HL7Exception[]) list.toArray(new HL7Exception[0]); 109 } 110 111 /** 112 * Retrieves the next message (setSource() must be called first). The next message 113 * is interpreted as everything up to the next blank line, not including 114 * C or C++ style comments (or blank lines themselves). An empty string 115 * indicates that there are no more messages. 116 */ 117 public String getNextMessage() throws IOException { 118 if (this.source == null) throw new IOException("Message source is null -- call setSource() first"); 119 120 StringBuffer message = new StringBuffer(); 121 boolean started = false; //got at least one non-blank line 122 boolean finished = false; //got a blank line after started, or end of stream 123 while (!finished) { 124 String line = this.source.readLine(); 125 if (line == null || (started && line.trim().length() == 0)) { 126 finished = true; 127 } else { 128 if (line.trim().length() > 0) { 129 started = true; 130 message.append(line); 131 message.append("\r"); 132 } 133 } 134 } 135 if (message.toString().trim().length() == 0) { 136 return ""; 137 } else { 138 return message.toString(); // can't trim by default (will omit final end-segment) 139 } 140 } 141 142 /** 143 * Command line tool for testing messages in files. 144 */ 145 public static void main(String args[]) { 146 if (args.length != 1 147 || args[0].equalsIgnoreCase("-?") 148 || args[0].equalsIgnoreCase("-h") 149 || args[0].equalsIgnoreCase("-help")) { 150 System.out.println("USAGE:"); 151 System.out.println(" ParseTester <source>"); 152 System.out.println(); 153 System.out.println(" <source> must be either a file containing HL7 messages or a directory containing such files"); 154 System.out.println(); 155 System.out.println("Notes:"); 156 System.out.println(" - Messages can be XML or ER7 encoded. "); 157 System.out.println(" - If there are multiple messages in a file they must be delimited by blank lines"); 158 System.out.println(" - C and C++ style comments are skipped"); 159 160 } else { 161 try { 162 System.out.println("Testing ... "); 163 File source = new File(args[0]); 164 ParseTester tester = new ParseTester(); 165 HL7Exception[] exceptions = tester.testAll(source); 166 if (exceptions.length > 0) System.out.println("Parsing problems with tested messages: "); 167 for (int i = 0; i < exceptions.length; i++) { 168 System.out.println("PROBLEM #" + (i+1)); 169 System.out.println(exceptions[i].getMessage()); 170 } 171 } catch (IOException e) { 172 System.out.println("Testing failed to complete because of a problem reading source file(s) ... \r\n"); 173 e.printStackTrace(); 174 } 175 } 176 } 177 178 /** 179 * Removes C and C++ style comments from a reader stream. C style comments are 180 * distinguished from URL protocol delimiters by the preceding colon in the 181 * latter. 182 */ 183 public static class CommentFilterReader extends PushbackReader { 184 185 private final char[] startCPPComment = {'/', '*'}; 186 private final char[] endCPPComment = {'*', '/'}; 187 private final char[] startCComment = {'/', '/'}; 188 private final char[] endCComment = {'\n'}; 189 private final char[] protocolDelim = {':', '/', '/'}; 190 191 public CommentFilterReader(Reader in) { 192 super(in, 5); 193 } 194 195 /** 196 * Returns the next character, not including comments. 197 */ 198 public int read() throws IOException { 199 if (atSequence(protocolDelim)) { 200 //proceed normally 201 } else if (atSequence(startCPPComment)) { 202 //skip() doesn't seem to work for some reason 203 while (!atSequence(endCPPComment)) super.read(); 204 for (int i = 0; i < endCPPComment.length; i++) super.read(); 205 } else if (atSequence(startCComment)) { 206 while (!atSequence(endCComment)) super.read(); 207 for (int i = 0; i < endCComment.length; i++) super.read(); 208 } 209 return super.read(); 210 } 211 212 public int read(char[] cbuf, int off, int len) throws IOException { 213 int i = -1; 214 boolean done = false; 215 while (++i < len) { 216 int next = read(); 217 if (next == 65535 || next == -1) { //Pushback causes -1 to convert to 65535 218 done = true; 219 break; 220 } 221 cbuf[off + i] = (char) next; 222 } 223 if (i == 0 && done) i = -1; 224 return i; 225 } 226 227 /** 228 * Tests incoming data for match with char sequence, resets reader when done. 229 */ 230 private boolean atSequence(char[] sequence) throws IOException { 231 boolean result = true; 232 int i = -1; 233 int[] data = new int[sequence.length]; 234 while (++i < sequence.length && result == true) { 235 data[i] = super.read(); 236 if ((char) data[i] != sequence[i]) result = false; //includes case where end of stream reached 237 } 238 for (int j = i-1; j >= 0; j--) { 239 this.unread(data[j]); 240 } 241 return result; 242 } 243 } 244 }