001    /**
002    The contents of this file are subject to the Mozilla Public License Version 1.1
003    (the "License"); you may not use this file except in compliance with the License.
004    You may obtain a copy of the License at http://www.mozilla.org/MPL/
005    Software distributed under the License is distributed on an "AS IS" basis,
006    WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
007    specific language governing rights and limitations under the License.
008    
009    The Initial Developer of the Original Code is University Health Network. Copyright (C)
010    2001.  All Rights Reserved.
011    
012    Contributor(s): ______________________________________.
013    
014    Alternatively, the contents of this file may be used under the terms of the
015    GNU General Public License (the  ???GPL???), in which case the provisions of the GPL are
016    applicable instead of those above.  If you wish to allow use of your version of this
017    file only under the terms of the GPL and not to allow others to use your version
018    of this file under the MPL, indicate your decision by deleting  the provisions above
019    and replace  them with the notice and other provisions required by the GPL License.
020    If you do not delete the provisions above, a recipient may use your version of
021    this file under either the MPL or the GPL.
022    
023    */
024    package ca.uhn.hl7v2.parser;
025    
026    import java.io.File;
027    import java.io.FileReader;
028    import java.util.ArrayList;
029    import java.util.List;
030    
031    import javax.xml.parsers.DocumentBuilderFactory;
032    
033    import org.w3c.dom.DOMException;
034    import org.w3c.dom.Document;
035    import org.w3c.dom.Element;
036    import org.w3c.dom.Node;
037    import org.w3c.dom.NodeList;
038    
039    import ca.uhn.hl7v2.HL7Exception;
040    import ca.uhn.hl7v2.model.Group;
041    import ca.uhn.hl7v2.model.Message;
042    import ca.uhn.hl7v2.model.Segment;
043    import ca.uhn.hl7v2.model.Structure;
044    import ca.uhn.log.HapiLog;
045    import ca.uhn.log.HapiLogFactory;
046    
047    /**
048     * <p>A default XMLParser.  This class assigns segment elements (in an XML-encoded message) 
049     * to Segment objects (in a Message object) using the name of a segment and the names 
050     * of any groups in which the segment is nested.  The names of group classes must correspond
051     * to the names of group elements (they must be identical except that a dot in the element 
052     * name, following the message name, is replaced with an underscore, in order to consitute a 
053     * valid class name). </p>
054     * <p>At the time of writing, the group names in the XML spec are changing.  Many of the group 
055     * names have been automatically generated based on the group contents.  However, these automatic 
056     * names are gradually being replaced with manually assigned names.  This process is expected to 
057     * be complete by November 2002.  As a result, mismatches are likely.  Messages could be  
058     * transformed prior to parsing (using XSLT) as a work-around.  Alternatively the group class names 
059     * could be changed to reflect updates in the XML spec.  Ultimately, HAPI group classes will be 
060     * changed to correspond with the official group names, once these are all assigned.  </p>
061     * @author Bryan Tripp
062     */
063    public class DefaultXMLParser extends XMLParser {
064    
065        private static final HapiLog log = HapiLogFactory.getHapiLog(DefaultXMLParser.class);
066    
067        /** Creates a new instance of DefaultXMLParser */
068        public DefaultXMLParser() {
069        }
070    
071        /** 
072         * Creates a new instance of DefaultXMLParser 
073         *  
074         * @param theFactory custom factory to use for model class lookup 
075         */
076        public DefaultXMLParser(ModelClassFactory theFactory) {
077            super(theFactory);
078        }
079        
080        /**
081         * <p>Creates an XML Document that corresponds to the given Message object. </p>
082         * <p>If you are implementing this method, you should create an XML Document, and insert XML Elements
083         * into it that correspond to the groups and segments that belong to the message type that your subclass
084         * of XMLParser supports.  Then, for each segment in the message, call the method
085         * <code>encode(Segment segmentObject, Element segmentElement)</code> using the Element for
086         * that segment and the corresponding Segment object from the given Message.</p>
087         */
088        public Document encodeDocument(Message source) throws HL7Exception {
089            String messageClassName = source.getClass().getName();
090            String messageName = messageClassName.substring(messageClassName.lastIndexOf('.') + 1);
091            org.w3c.dom.Document doc = null;
092            try {
093                doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
094                Element root = doc.createElement(messageName);
095                doc.appendChild(root);
096            }
097            catch (Exception e) {
098                throw new HL7Exception(
099                    "Can't create XML document - " + e.getClass().getName(),
100                    HL7Exception.APPLICATION_INTERNAL_ERROR,
101                    e);
102            }
103            encode(source, doc.getDocumentElement());
104            return doc;
105        }
106    
107        /**
108         * Copies data from a group object into the corresponding group element, creating any 
109         * necessary child nodes.  
110         */
111        private void encode(ca.uhn.hl7v2.model.Group groupObject, org.w3c.dom.Element groupElement) throws HL7Exception {
112            String[] childNames = groupObject.getNames();
113            String messageName = groupObject.getMessage().getName();
114            
115            try {
116                for (int i = 0; i < childNames.length; i++) {
117                    Structure[] reps = groupObject.getAll(childNames[i]);
118                    for (int j = 0; j < reps.length; j++) {
119                        Element childElement =
120                            groupElement.getOwnerDocument().createElement(makeGroupElementName(messageName, childNames[i]));
121                        groupElement.appendChild(childElement);
122                        if (reps[j] instanceof Group) {
123                            encode((Group) reps[j], childElement);
124                        }
125                        else if (reps[j] instanceof Segment) {
126                            encode((Segment) reps[j], childElement);
127                        }
128                    }
129                }
130            }
131            catch (DOMException e) {
132                throw new HL7Exception(
133                    "Can't encode group " + groupObject.getClass().getName(),
134                    HL7Exception.APPLICATION_INTERNAL_ERROR,
135                    e);
136            }
137        }
138    
139        /**
140         * <p>Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message.</p>
141         * <p>The easiest way to implement this method for a particular message structure is as follows:
142         * <ol><li>Create an instance of the Message type you are going to handle with your subclass
143         * of XMLParser</li>
144         * <li>Go through the given Document and find the Elements that represent the top level of
145         * each message segment. </li>
146         * <li>For each of these segments, call <code>parse(Segment segmentObject, Element segmentElement)</code>,
147         * providing the appropriate Segment from your Message object, and the corresponding Element.</li></ol>
148         * At the end of this process, your Message object should be populated with data from the XML
149         * Document.</p>
150         * @throws HL7Exception if the message is not correctly formatted.
151         * @throws EncodingNotSupportedException if the message encoded
152         *     is not supported by this parser.
153         */
154        public Message parseDocument(org.w3c.dom.Document XMLMessage, String version) throws HL7Exception {
155            String messageName = XMLMessage.getDocumentElement().getTagName();
156            Message message = instantiateMessage(messageName, version, true);
157            parse(message, XMLMessage.getDocumentElement());
158            return message;
159        }
160    
161        /**
162         * Populates the given group object with data from the given group element, ignoring 
163         * any unrecognized nodes.  
164         */
165        private void parse(ca.uhn.hl7v2.model.Group groupObject, org.w3c.dom.Element groupElement) throws HL7Exception {
166            String[] childNames = groupObject.getNames();
167            String messageName = groupObject.getMessage().getName();
168            
169            NodeList allChildNodes = groupElement.getChildNodes();
170            ArrayList unparsedElementList = new ArrayList();
171            for (int i = 0; i < allChildNodes.getLength(); i++) {
172                Node node = allChildNodes.item(i);
173                String name = node.getNodeName();
174                if (node.getNodeType() == Node.ELEMENT_NODE && !unparsedElementList.contains(name)) {
175                    unparsedElementList.add(name);                
176                }
177            }
178            
179            //we're not too fussy about order here (all occurrences get parsed as repetitions) ... 
180            for (int i = 0; i < childNames.length; i++) {
181                unparsedElementList.remove(childNames[i]);
182                
183                // 4 char segment names are second occurrences of a segment within a single message
184                // structure. e.g. the second PID segment in an A17 patient swap message is known
185                // to hapi's code represenation as PID2
186                if (childNames[i].length() != 4) {   
187                    parseReps(groupElement, groupObject, messageName, childNames[i], childNames[i]);
188                } else {
189                    log.debug("Skipping rep segment: " + childNames[i]);
190                }
191            }
192            
193            for (int i = 0; i < unparsedElementList.size(); i++) {
194                String segName = (String) unparsedElementList.get(i);            
195                String segIndexName = groupObject.addNonstandardSegment(segName);
196                parseReps(groupElement, groupObject, messageName, segName, segIndexName);
197            }
198        }
199        
200        //param childIndexName may have an integer on the end if >1 sibling with same name (e.g. NTE2) 
201        private void parseReps(Element groupElement, Group groupObject, 
202                String messageName, String childName, String childIndexName) throws HL7Exception {
203            
204            List reps = getChildElementsByTagName(groupElement, makeGroupElementName(messageName, childName));
205            log.debug("# of elements matching " 
206                + makeGroupElementName(messageName, childName) + ": " + reps.size());
207    
208                    if (groupObject.isRepeating(childIndexName)) {
209                            for (int i = 0; i < reps.size(); i++) {
210                                    parseRep((Element) reps.get(i), groupObject.get(childIndexName, i));
211                            }                                       
212                    } else {
213                            if (reps.size() > 0) {
214                                    parseRep((Element) reps.get(0), groupObject.get(childIndexName, 0));                            
215                            }
216    
217    //                      if (reps.size() > 1) {                       
218    //                              String newIndexName = groupObject.addNonstandardSegment(childName);                     
219    //                              for (int i = 1; i < reps.size(); i++) {
220    //                                      parseRep((Element) reps.get(i), groupObject.get(newIndexName, i-1));
221    //                              }                                                               
222    //                      }
223                            if (reps.size() > 1) {
224                                    String newIndexName = "";
225                                    int i=1;
226                                    try     {
227                                            for (i = 1; i < reps.size(); i++) {
228                                                    newIndexName = childName+(i+1);
229                                                    Structure st = groupObject.get(newIndexName);
230                                                    parseRep((Element) reps.get(i), st);
231                                            }
232                                    } catch(Throwable t) {
233                                            log.info("Issue Parsing: " + t);
234                                            newIndexName = groupObject.addNonstandardSegment(childName);
235                                            for (int j = i; j < reps.size(); j++) {
236                                                    parseRep((Element) reps.get(j), groupObject.get(newIndexName, j-i));
237                                            }
238                                    }
239                            }
240                            
241                    }
242        }
243        
244        private void parseRep(Element theElem, Structure theObj) throws HL7Exception {
245                    if (theObj instanceof Group) {
246                            parse((Group) theObj, theElem);
247                    }
248                    else if (theObj instanceof Segment) {
249                            parse((Segment) theObj, theElem);
250                    }                
251                    log.debug("Parsed element: " + theElem.getNodeName());          
252        }
253        
254        //includes direct children only
255        private List getChildElementsByTagName(Element theElement, String theName) {
256            List result = new ArrayList(10);
257            NodeList children = theElement.getChildNodes();
258            
259            for (int i = 0; i < children.getLength(); i++) {
260                    Node child = children.item(i);
261                    if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(theName)) {
262                            result.add(child);
263                    }
264            }
265            
266            return result; 
267        }
268        
269        /** 
270         * Given the name of a group element in an XML message, returns the corresponding 
271         * group class name.  This name is identical except in order to be a valid class 
272         * name, the dot character immediately following the message name is replaced with 
273         * an underscore.  For example, there is a group element called ADT_A01.INSURANCE and the 
274         * corresponding group Class is called ADT_A01_INSURANCE. 
275         */
276    //    protected static String makeGroupClassName(String elementName) {
277    //        return elementName.replace('.', '_');
278    //    }
279    
280        /** 
281         * Given the name of a message and a Group class, returns the corresponding group element name in an 
282         * XML-encoded message.  This is the message name and group name separated by a dot. For example, 
283         * ADT_A01.INSURANCE.
284         * 
285         * If it looks like a segment name (i.e. has 3 characters), no change is made. 
286         */
287        protected static String makeGroupElementName(String messageName, String className) {
288            String ret = null;
289    
290            if (className.length() > 4) {
291                StringBuffer elementName = new StringBuffer();
292                elementName.append(messageName);
293                elementName.append('.');
294                elementName.append(className);
295                ret = elementName.toString();
296            } else if (className.length() == 4) {
297                ret = className.substring(0,3);
298            } else {
299                ret = className;
300            }
301            
302            return ret;
303        }
304    
305        /** Test harness */
306        public static void main(String args[]) {
307            if (args.length != 1) {
308                System.out.println("Usage: DefaultXMLParser pipe_encoded_file");
309                System.exit(1);
310            }
311    
312            //read and parse message from file 
313            try {
314                File messageFile = new File(args[0]);
315                long fileLength = messageFile.length();
316                FileReader r = new FileReader(messageFile);
317                char[] cbuf = new char[(int) fileLength];
318                System.out.println("Reading message file ... " + r.read(cbuf) + " of " + fileLength + " chars");
319                r.close();
320                String messString = String.valueOf(cbuf);
321    
322                Parser inParser = null;
323                Parser outParser = null;
324                PipeParser pp = new PipeParser();
325                ca.uhn.hl7v2.parser.XMLParser xp = new DefaultXMLParser();
326                System.out.println("Encoding: " + pp.getEncoding(messString));
327                if (pp.getEncoding(messString) != null) {
328                    inParser = pp;
329                    outParser = xp;
330                }
331                else if (xp.getEncoding(messString) != null) {
332                    inParser = xp;
333                    outParser = pp;
334                }
335    
336                Message mess = inParser.parse(messString);
337                System.out.println("Got message of type " + mess.getClass().getName());
338    
339                String otherEncoding = outParser.encode(mess);
340                System.out.println(otherEncoding);
341            }
342            catch (Exception e) {
343                e.printStackTrace();
344            }
345        }
346    
347    
348    }