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.slf4j.Logger;
042    import org.slf4j.LoggerFactory;
043    import org.w3c.dom.Document;
044    import org.w3c.dom.Element;
045    import org.w3c.dom.Node;
046    import org.w3c.dom.NodeList;
047    import org.xml.sax.EntityResolver;
048    import org.xml.sax.ErrorHandler;
049    import org.xml.sax.InputSource;
050    import org.xml.sax.SAXException;
051    import org.xml.sax.SAXParseException;
052    
053    import ca.uhn.hl7v2.conf.ProfileException;
054    import ca.uhn.hl7v2.conf.spec.MetaData;
055    import ca.uhn.hl7v2.conf.spec.RuntimeProfile;
056    import ca.uhn.hl7v2.conf.spec.message.AbstractComponent;
057    import ca.uhn.hl7v2.conf.spec.message.AbstractSegmentContainer;
058    import ca.uhn.hl7v2.conf.spec.message.Component;
059    import ca.uhn.hl7v2.conf.spec.message.DataValue;
060    import ca.uhn.hl7v2.conf.spec.message.Field;
061    import ca.uhn.hl7v2.conf.spec.message.ProfileStructure;
062    import ca.uhn.hl7v2.conf.spec.message.Seg;
063    import ca.uhn.hl7v2.conf.spec.message.SegGroup;
064    import ca.uhn.hl7v2.conf.spec.message.StaticDef;
065    import 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     */
097    public 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    }