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    }