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