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 Original Code is "ProfileParser.java".  Description: 
010    "Parses a Message Profile XML document into a RuntimeProfile object." 
011    
012    The Initial Developer of the Original Code is University Health Network. Copyright (C) 
013    2003.  All Rights Reserved. 
014    
015    Contributor(s): ______________________________________. 
016    
017    Alternatively, the contents of this file may be used under the terms of the 
018    GNU General Public License (the  ?GPL?), in which case the provisions of the GPL are 
019    applicable instead of those above.  If you wish to allow use of your version of this 
020    file only under the terms of the GPL and not to allow others to use your version 
021    of this file under the MPL, indicate your decision by deleting  the provisions above 
022    and replace  them with the notice and other provisions required by the GPL License.  
023    If you do not delete the provisions above, a recipient may use your version of 
024    this file under either the MPL or the GPL. 
025    
026    */
027    
028    package ca.uhn.hl7v2.conf.parser;
029    
030    import java.io.BufferedReader;
031    import java.io.File;
032    import java.io.FileNotFoundException;
033    import java.io.FileReader;
034    import java.io.IOException;
035    import java.io.InputStream;
036    import java.io.InputStreamReader;
037    import java.io.StringReader;
038    
039    import org.apache.xerces.parsers.DOMParser;
040    import org.apache.xerces.parsers.StandardParserConfiguration;
041    import org.w3c.dom.Document;
042    import org.w3c.dom.Element;
043    import org.w3c.dom.Node;
044    import org.w3c.dom.NodeList;
045    import org.xml.sax.EntityResolver;
046    import org.xml.sax.ErrorHandler;
047    import org.xml.sax.InputSource;
048    import org.xml.sax.SAXException;
049    import org.xml.sax.SAXParseException;
050    
051    import ca.uhn.hl7v2.conf.ProfileException;
052    import ca.uhn.hl7v2.conf.spec.MetaData;
053    import ca.uhn.hl7v2.conf.spec.RuntimeProfile;
054    import ca.uhn.hl7v2.conf.spec.message.AbstractComponent;
055    import ca.uhn.hl7v2.conf.spec.message.AbstractSegmentContainer;
056    import ca.uhn.hl7v2.conf.spec.message.Component;
057    import ca.uhn.hl7v2.conf.spec.message.DataValue;
058    import ca.uhn.hl7v2.conf.spec.message.Field;
059    import ca.uhn.hl7v2.conf.spec.message.ProfileStructure;
060    import ca.uhn.hl7v2.conf.spec.message.Seg;
061    import ca.uhn.hl7v2.conf.spec.message.SegGroup;
062    import ca.uhn.hl7v2.conf.spec.message.StaticDef;
063    import ca.uhn.hl7v2.conf.spec.message.SubComponent;
064    import ca.uhn.log.HapiLog;
065    import ca.uhn.log.HapiLogFactory;
066    
067    /**
068     * Parses a Message Profile XML document into a RuntimeProfile object.  A 
069     * Message Profile is a formal description of additional constraints on a 
070     * message (beyond what is specified in the HL7 specification), usually for  
071     * a particular system, region, etc.  Message profiles are introduced in 
072     * HL7 version 2.5 section 2.12.  The RuntimeProfile object is simply an 
073     * object representation of the profile, which may be used for validating  
074     * messages or editing the profile.   
075     * @author Bryan Tripp
076     */
077    public class ProfileParser {
078    
079        private static final HapiLog log = HapiLogFactory.getHapiLog(ProfileParser.class);
080    
081        private DOMParser parser;
082        private boolean alwaysValidate;
083    
084        /** 
085         * Creates a new instance of ProfileParser 
086         * @param alwaysValidateAgainstDTD if true, validates all profiles against a 
087         *      local copy of the profile DTD; if false, validates against declared 
088         *      grammar (if any)
089         */
090        public ProfileParser(boolean alwaysValidateAgainstDTD) {
091    
092            this.alwaysValidate = alwaysValidateAgainstDTD;
093    
094            parser = new DOMParser(new StandardParserConfiguration());
095            try {
096                parser.setFeature("http://apache.org/xml/features/dom/include-ignorable-whitespace", false);
097            }
098            catch (Exception e) {
099                log.error("Can't exclude whitespace from XML DOM", e);
100            }
101            try {
102                parser.setFeature("http://apache.org/xml/features/validation/dynamic", true);
103            }
104            catch (Exception e) {
105                log.error("Can't validate profile against XML grammar", e);
106            }
107            parser.setErrorHandler(new ErrorHandler() {
108                public void error(SAXParseException e) throws SAXException {
109                    throw e;
110                }
111                public void fatalError(SAXParseException e) throws SAXException {
112                    throw e;
113                }
114                public void warning(SAXParseException e) throws SAXException {
115                    System.err.println("Warning: " + e.getMessage());
116                }
117    
118            });
119    
120            if (alwaysValidateAgainstDTD) {
121                try {
122                    final String grammar = loadGrammar();
123                    parser.setEntityResolver(new EntityResolver() {
124                        //returns the grammar we specify no matter what the document declares
125                        public InputSource resolveEntity(String publicID, String systemID)
126                            throws SAXException, IOException {
127                            return new InputSource(new StringReader(grammar));
128                        }
129                    });
130                }
131                catch (IOException e) {
132                    log.error("Can't validate profiles against XML grammar", e);
133                }
134            }
135    
136        }
137    
138        /** Loads the XML grammar from disk */
139        private String loadGrammar() throws IOException {
140            InputStream dtdStream =
141                ProfileParser.class.getClassLoader().getResourceAsStream("ca/uhn/hl7v2/conf/parser/message_profile.dtd");
142            BufferedReader dtdReader = new BufferedReader(new InputStreamReader(dtdStream));
143            String line = null;
144            StringBuffer dtd = new StringBuffer();
145            while ((line = dtdReader.readLine()) != null) {
146                dtd.append(line);
147                dtd.append("\r\n");
148            }
149            return dtd.toString();
150        }
151    
152        
153        /**
154         * Parses an XML profile string into a RuntimeProfile object.
155         * 
156         * Input is a path pointing to a textual file on the classpath. Note that
157         * the file will be read using the thread context class loader.
158         * 
159         * For example, if you had a file called PROFILE.TXT in package com.foo.stuff,
160         * you would pass in "com/foo/stuff/PROFILE.TXT"
161         * 
162         * @throws IOException If the resource can't be read
163         */
164        public RuntimeProfile parseClasspath(String classPath) throws ProfileException, IOException {
165            
166            InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPath);
167            if (stream == null) {
168                    throw new FileNotFoundException(classPath);
169            }
170            
171            StringBuffer profileString = new StringBuffer();
172            byte[] buffer = new byte[1000];
173            int bytesRead;
174            while ((bytesRead = stream.read(buffer)) > 0) {
175                    profileString.append(new String(buffer, 0, bytesRead));
176            }
177            
178            RuntimeProfile profile = new RuntimeProfile();
179            Document doc = parseIntoDOM(profileString.toString());
180    
181            Element root = doc.getDocumentElement();
182            profile.setHL7Version(root.getAttribute("HL7Version"));
183    
184            //get static definition
185            NodeList nl = root.getElementsByTagName("HL7v2xStaticDef");
186            Element staticDef = (Element) nl.item(0);
187            StaticDef sd = parseStaticProfile(staticDef);
188            profile.setMessage(sd);
189            return profile;
190        }
191    
192        
193        /**
194         * Parses an XML profile string into a RuntimeProfile object.  
195         */
196        public RuntimeProfile parse(String profileString) throws ProfileException {
197            RuntimeProfile profile = new RuntimeProfile();
198            Document doc = parseIntoDOM(profileString);
199    
200            Element root = doc.getDocumentElement();
201            profile.setHL7Version(root.getAttribute("HL7Version"));
202    
203            //get static definition
204            NodeList nl = root.getElementsByTagName("HL7v2xStaticDef");
205            Element staticDef = (Element) nl.item(0);
206            StaticDef sd = parseStaticProfile(staticDef);
207            profile.setMessage(sd);
208            return profile;
209        }
210    
211        private StaticDef parseStaticProfile(Element elem) throws ProfileException {
212            StaticDef message = new StaticDef();
213            message.setMsgType(elem.getAttribute("MsgType"));
214            message.setEventType(elem.getAttribute("EventType"));
215            message.setMsgStructID(elem.getAttribute("MsgStructID"));
216            message.setOrderControl(elem.getAttribute("OrderControl"));
217            message.setEventDesc(elem.getAttribute("EventDesc"));
218            message.setIdentifier(elem.getAttribute("identifier"));
219            message.setRole(elem.getAttribute("role"));
220    
221            Element md = getFirstElementByTagName("MetaData", elem);
222            if (md != null)
223                message.setMetaData(parseMetaData(md));
224    
225            message.setImpNote(getValueOfFirstElement("ImpNote", elem));
226            message.setDescription(getValueOfFirstElement("Description", elem));
227            message.setReference(getValueOfFirstElement("Reference", elem));
228    
229            parseChildren(message, elem);
230            return message;
231        }
232    
233        /** Parses metadata element */
234        private MetaData parseMetaData(Element elem) {
235            log.debug("ProfileParser.parseMetaData() has been called ... note that this method does nothing.");
236            return null;
237        }
238    
239        /** Parses children (i.e. segment groups, segments) of a segment group or message profile */
240        private void parseChildren(AbstractSegmentContainer parent, Element elem) throws ProfileException {
241            int childIndex = 1;
242            NodeList children = elem.getChildNodes();
243            for (int i = 0; i < children.getLength(); i++) {
244                Node n = children.item(i);
245                if (n.getNodeType() == Node.ELEMENT_NODE) {
246                    Element child = (Element) n;
247                    if (child.getNodeName().equalsIgnoreCase("SegGroup")) {
248                        SegGroup group = parseSegmentGroupProfile(child);
249                        parent.setChild(childIndex++, group);
250                    }
251                    else if (child.getNodeName().equalsIgnoreCase("Segment")) {
252                        Seg segment = parseSegmentProfile(child);
253                        parent.setChild(childIndex++, segment);
254                    }
255                }
256            }
257        }
258    
259        /** Parses a segment group profile */
260        private SegGroup parseSegmentGroupProfile(Element elem) throws ProfileException {
261            SegGroup group = new SegGroup();
262            log.debug("Parsing segment group profile: " + elem.getAttribute("Name"));
263    
264            parseProfileStuctureData(group, elem);
265    
266            parseChildren(group, elem);
267            return group;
268        }
269    
270        /** Parses a segment profile */
271        private Seg parseSegmentProfile(Element elem) throws ProfileException {
272            Seg segment = new Seg();
273            log.debug("Parsing segment profile: " + elem.getAttribute("Name"));
274    
275            parseProfileStuctureData(segment, elem);
276    
277            int childIndex = 1;
278            NodeList children = elem.getChildNodes();
279            for (int i = 0; i < children.getLength(); i++) {
280                Node n = children.item(i);
281                if (n.getNodeType() == Node.ELEMENT_NODE) {
282                    Element child = (Element) n;
283                    if (child.getNodeName().equalsIgnoreCase("Field")) {
284                        Field field = parseFieldProfile(child);
285                        segment.setField(childIndex++, field);
286                    }
287                }
288            }
289    
290            return segment;
291        }
292    
293        /** Parse common data in profile structure (eg SegGroup, Segment) */
294        private void parseProfileStuctureData(ProfileStructure struct, Element elem) throws ProfileException {
295            struct.setName(elem.getAttribute("Name"));
296            struct.setLongName(elem.getAttribute("LongName"));
297            struct.setUsage(elem.getAttribute("Usage"));
298            String min = elem.getAttribute("Min");
299            String max = elem.getAttribute("Max");
300            try {
301                struct.setMin(Short.parseShort(min));
302                if (max.indexOf('*') >= 0) {
303                    struct.setMax((short) - 1);
304                }
305                else {
306                    struct.setMax(Short.parseShort(max));
307                }
308            }
309            catch (NumberFormatException e) {
310                throw new ProfileException("Min and max must be short integers: " + min + ", " + max, e);
311            }
312    
313            struct.setImpNote(getValueOfFirstElement("ImpNote", elem));
314            struct.setDescription(getValueOfFirstElement("Description", elem));
315            struct.setReference(getValueOfFirstElement("Reference", elem));
316            struct.setPredicate(getValueOfFirstElement("Predicate", elem));
317        }
318    
319        /** Parses a field profile */
320        private Field parseFieldProfile(Element elem) throws ProfileException {
321            Field field = new Field();
322            log.debug("  Parsing field profile: " + elem.getAttribute("Name"));
323    
324            field.setUsage(elem.getAttribute("Usage"));
325            String itemNo = elem.getAttribute("ItemNo");
326            String min = elem.getAttribute("Min");
327            String max = elem.getAttribute("Max");
328    
329            try {
330                if (itemNo.length() > 0) {    
331                    field.setItemNo(Short.parseShort(itemNo));
332                }
333            }
334            catch (NumberFormatException e) {
335                throw new ProfileException("Invalid ItemNo: " + itemNo + "( for name " + elem.getAttribute("Name") + ")", e);
336            } // try-catch
337            
338            try {
339                field.setMin(Short.parseShort(min));
340                if (max.indexOf('*') >= 0) {
341                    field.setMax((short) - 1);
342                }
343                else {
344                    field.setMax(Short.parseShort(max));
345                }
346            }
347            catch (NumberFormatException e) {
348                throw new ProfileException("Min and max must be short integers: " + min + ", " + max, e);
349            }
350    
351            parseAbstractComponentData(field, elem);
352    
353            int childIndex = 1;
354            NodeList children = elem.getChildNodes();
355            for (int i = 0; i < children.getLength(); i++) {
356                Node n = children.item(i);
357                if (n.getNodeType() == Node.ELEMENT_NODE) {
358                    Element child = (Element) n;
359                    if (child.getNodeName().equalsIgnoreCase("Component")) {
360                        Component comp = (Component) parseComponentProfile(child, false);
361                        field.setComponent(childIndex++, comp);
362                    }
363                }
364            }
365    
366            return field;
367        }
368    
369        /** Parses a component profile */
370        private AbstractComponent parseComponentProfile(Element elem, boolean isSubComponent) throws ProfileException {
371            AbstractComponent comp = null;
372            if (isSubComponent) {
373                log.debug("      Parsing subcomp profile: " + elem.getAttribute("Name"));
374                comp = new SubComponent();
375            }
376            else {
377                log.debug("    Parsing comp profile: " + elem.getAttribute("Name"));
378                comp = new Component();
379    
380                int childIndex = 1;
381                NodeList children = elem.getChildNodes();
382                for (int i = 0; i < children.getLength(); i++) {
383                    Node n = children.item(i);
384                    if (n.getNodeType() == Node.ELEMENT_NODE) {
385                        Element child = (Element) n;
386                        if (child.getNodeName().equalsIgnoreCase("SubComponent")) {
387                            SubComponent subcomp = (SubComponent) parseComponentProfile(child, true);
388                            ((Component) comp).setSubComponent(childIndex++, subcomp);
389                        }
390                    }
391                }
392            }
393    
394            parseAbstractComponentData(comp, elem);
395    
396            return comp;
397        }
398    
399        /** Parses common features of AbstractComponents (ie field, component, subcomponent) */
400        private void parseAbstractComponentData(AbstractComponent comp, Element elem) throws ProfileException {
401            comp.setName(elem.getAttribute("Name"));
402            comp.setUsage(elem.getAttribute("Usage"));
403            comp.setDatatype(elem.getAttribute("Datatype"));
404            String length = elem.getAttribute("Length");
405            if (length != null && length.length() > 0) {
406                try {
407                    comp.setLength(Long.parseLong(length));
408                }
409                catch (NumberFormatException e) {
410                    throw new ProfileException("Length must be a long integer: " + length, e);
411                }
412            }
413            comp.setConstantValue(elem.getAttribute("ConstantValue"));
414            String table = elem.getAttribute("Table");
415            if (table != null && table.length() > 0) {
416                try {
417                    comp.setTable(table);
418                }
419                catch (NumberFormatException e) {
420                    throw new ProfileException("Table must be a short integer: " + table, e);
421                }
422            }
423    
424            comp.setImpNote(getValueOfFirstElement("ImpNote", elem));
425            comp.setDescription(getValueOfFirstElement("Description", elem));
426            comp.setReference(getValueOfFirstElement("Reference", elem));
427            comp.setPredicate(getValueOfFirstElement("Predicate", elem));
428    
429            int dataValIndex = 0;
430            NodeList children = elem.getChildNodes();
431            for (int i = 0; i < children.getLength(); i++) {
432                Node n = children.item(i);
433                if (n.getNodeType() == Node.ELEMENT_NODE) {
434                    Element child = (Element) n;
435                    if (child.getNodeName().equalsIgnoreCase("DataValues")) {
436                        DataValue val = new DataValue();
437                        val.setExValue(child.getAttribute("ExValue"));
438                        comp.setDataValues(dataValIndex++, val);
439                    }
440                }
441            }
442    
443        }
444    
445        /** Parses profile string into DOM document */
446        private Document parseIntoDOM(String profileString) throws ProfileException {
447            if (this.alwaysValidate)
448                profileString = insertDoctype(profileString);
449            Document doc = null;
450            try {
451                synchronized (this) {
452                    parser.parse(new InputSource(new StringReader(profileString)));
453                    log.debug("DOM parse complete");
454                    doc = parser.getDocument();
455                }
456            }
457            catch (SAXException se) {
458                throw new ProfileException("SAXException parsing message profile: " + se.getMessage());
459            }
460            catch (IOException ioe) {
461                throw new ProfileException("IOException parsing message profile: " + ioe.getMessage());
462            }
463            return doc;
464        }
465    
466        /** Inserts a DOCTYPE declaration in the string if there isn't one */
467        private String insertDoctype(String profileString) {
468            String result = profileString;
469            if (profileString.indexOf("<!DOCTYPE") < 0) {
470                StringBuffer buf = new StringBuffer();
471                int loc = profileString.indexOf("?>");
472                if (loc > 0) {
473                    buf.append(profileString.substring(0, loc + 2));
474                    buf.append("<!DOCTYPE HL7v2xConformanceProfile SYSTEM \"\">");
475                    buf.append(profileString.substring(loc + 2));
476                    result = buf.toString();
477                }
478            }
479            return result;
480        }
481    
482        /** 
483         * Returns the first child element of the given parent that matches the given 
484         * tag name.  Returns null if no instance of the expected element is present.  
485         */
486        private Element getFirstElementByTagName(String name, Element parent) {
487            NodeList nl = parent.getElementsByTagName(name);
488            Element ret = null;
489            if (nl.getLength() > 0) {
490                ret = (Element) nl.item(0);
491            }
492            return ret;
493        }
494    
495        /** 
496         * Gets the result of getFirstElementByTagName() and returns the 
497         * value of that element.  
498         */
499        private String getValueOfFirstElement(String name, Element parent) throws ProfileException {
500            Element el = getFirstElementByTagName(name, parent);
501            String val = null;
502            if (el != null) {
503                try {
504                    Node n = el.getFirstChild();
505                    if (n.getNodeType() == Node.TEXT_NODE) {
506                        val = n.getNodeValue();
507                    }
508                }
509                catch (Exception e) {
510                    throw new ProfileException("Unable to get value of node " + name, e);
511                }
512            }
513            return val;
514        }
515    
516        public static void main(String args[]) {
517    
518            if (args.length != 1) {
519                System.out.println("Usage: ProfileParser profile_file");
520                System.exit(1);
521            }
522    
523            try {
524                //File f = new File("C:\\Documents and Settings\\bryan\\hapilocal\\hapi\\ca\\uhn\\hl7v2\\conf\\parser\\example_ack.xml");
525                File f = new File(args[0]);
526                BufferedReader in = new BufferedReader(new FileReader(f));
527                char[] cbuf = new char[(int) f.length()];
528                in.read(cbuf, 0, (int) f.length());
529                String xml = String.valueOf(cbuf);
530                //System.out.println(xml);
531    
532                ProfileParser pp = new ProfileParser(true);
533                RuntimeProfile spec = pp.parse(xml);
534            }
535            catch (Exception e) {
536                e.printStackTrace();
537            }
538        }
539    
540    }