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