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;
036
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039import org.w3c.dom.DOMError;
040import org.w3c.dom.DOMErrorHandler;
041import org.w3c.dom.Document;
042import org.w3c.dom.Element;
043import org.w3c.dom.Node;
044import org.w3c.dom.NodeList;
045
046import ca.uhn.hl7v2.conf.ProfileException;
047import ca.uhn.hl7v2.conf.spec.MetaData;
048import ca.uhn.hl7v2.conf.spec.RuntimeProfile;
049import ca.uhn.hl7v2.conf.spec.message.AbstractComponent;
050import ca.uhn.hl7v2.conf.spec.message.AbstractSegmentContainer;
051import ca.uhn.hl7v2.conf.spec.message.Component;
052import ca.uhn.hl7v2.conf.spec.message.DataValue;
053import ca.uhn.hl7v2.conf.spec.message.Field;
054import ca.uhn.hl7v2.conf.spec.message.ProfileStructure;
055import ca.uhn.hl7v2.conf.spec.message.Seg;
056import ca.uhn.hl7v2.conf.spec.message.SegGroup;
057import ca.uhn.hl7v2.conf.spec.message.StaticDef;
058import ca.uhn.hl7v2.conf.spec.message.SubComponent;
059import ca.uhn.hl7v2.util.XMLUtils;
060
061/**
062 * <p>
063 * Parses a Message Profile XML document into a RuntimeProfile object. A Message Profile is a formal
064 * description of additional constraints on a message (beyond what is specified in the HL7
065 * specification), usually for a particular system, region, etc. Message profiles are introduced in
066 * HL7 version 2.5 section 2.12. The RuntimeProfile object is simply an object representation of the
067 * profile, which may be used for validating messages or editing the profile.
068 * </p>
069 * <p>
070 * Example usage: <code><pre>
071 *              // Load the profile from the classpath
072 *      ProfileParser parser = new ProfileParser(false);
073 *      RuntimeProfile profile = parser.parseClasspath("ca/uhn/hl7v2/conf/parser/example_ack.xml");
074 * 
075 *      // Create a message to validate
076 *      String message = "MSH|^~\\&|||||||ACK^A01|1|D|2.4|||||CAN|wrong|F^^HL70001^x^^HL78888|\r"; //note HL7888 doesn't exist
077 *      ACK msg = (ACK) (new PipeParser()).parse(message);
078 *              
079 *      // Validate
080 *              HL7Exception[] errors = new DefaultValidator().validate(msg, profile.getMessage());
081 *              
082 *              // Each exception is a validation error
083 *              System.out.println("Validation errors: " + Arrays.asList(errors));
084 * </pre></code>
085 * </p>
086 * 
087 * @author Bryan Tripp
088 */
089public class ProfileParser {
090
091        private static final String PROFILE_XSD = "ca/uhn/hl7v2/conf/parser/message_profile.xsd";
092
093        private static final Logger log = LoggerFactory.getLogger(ProfileParser.class);
094
095        private boolean alwaysValidate;
096        private DOMErrorHandler errorHandler;
097
098        /**
099         * Creates a new instance of ProfileParser
100         * 
101         * @param alwaysValidate if true, validates all profiles against a local copy of the
102         *            profile XSD; if false, validates against declared grammar (if any)
103         */
104        public ProfileParser(boolean alwaysValidate) {
105
106                this.alwaysValidate = alwaysValidate;
107                this.errorHandler = new DOMErrorHandler() {
108
109                        public boolean handleError(DOMError error) {
110                                if (error.getSeverity() == DOMError.SEVERITY_WARNING) {
111                                        log.warn("Warning: {}", error.getMessage());
112                                } else {
113                                        throw new RuntimeException((Exception) error.getRelatedException());
114                                }
115                                return true;
116                        }
117
118                };
119        }
120
121
122        /**
123         * Parses an XML profile string into a RuntimeProfile object.
124         * 
125         * Input is a path pointing to a textual file on the classpath. Note that the file will be read
126         * using the thread context class loader.
127         * 
128         * For example, if you had a file called PROFILE.TXT in package com.foo.stuff, you would pass in
129         * "com/foo/stuff/PROFILE.TXT"
130         * 
131         * @throws IOException If the resource can't be read
132         */
133        public RuntimeProfile parseClasspath(String classPath) throws ProfileException, IOException {
134
135                InputStream stream = Thread.currentThread().getContextClassLoader()
136                                .getResourceAsStream(classPath);
137                if (stream == null) {
138                        throw new FileNotFoundException(classPath);
139                }
140
141                StringBuffer profileString = new StringBuffer();
142                byte[] buffer = new byte[1000];
143                int bytesRead;
144                while ((bytesRead = stream.read(buffer)) > 0) {
145                        profileString.append(new String(buffer, 0, bytesRead));
146                }
147
148                RuntimeProfile profile = new RuntimeProfile();
149                Document doc = parseIntoDOM(profileString.toString());
150
151                Element root = doc.getDocumentElement();
152                profile.setHL7Version(root.getAttribute("HL7Version"));
153
154                // get static definition
155                NodeList nl = root.getElementsByTagName("HL7v2xStaticDef");
156                Element staticDef = (Element) nl.item(0);
157                StaticDef sd = parseStaticProfile(staticDef);
158                profile.setMessage(sd);
159                return profile;
160        }
161
162        /**
163         * Parses an XML profile string into a RuntimeProfile object.
164         */
165        public RuntimeProfile parse(String profileString) throws ProfileException {
166                RuntimeProfile profile = new RuntimeProfile();
167                Document doc = parseIntoDOM(profileString);
168
169                Element root = doc.getDocumentElement();
170                profile.setHL7Version(root.getAttribute("HL7Version"));
171
172                NodeList metadataList = root.getElementsByTagName("MetaData");
173                if (metadataList.getLength() > 0) {
174                        Element metadata = (Element) metadataList.item(0);
175                        String name = metadata.getAttribute("Name");
176                        profile.setName(name);
177                }
178
179                // get static definition
180                NodeList nl = root.getElementsByTagName("HL7v2xStaticDef");
181                Element staticDef = (Element) nl.item(0);
182                StaticDef sd = parseStaticProfile(staticDef);
183                profile.setMessage(sd);
184                return profile;
185        }
186
187        private StaticDef parseStaticProfile(Element elem) throws ProfileException {
188                StaticDef message = new StaticDef();
189                message.setMsgType(elem.getAttribute("MsgType"));
190                message.setEventType(elem.getAttribute("EventType"));
191                message.setMsgStructID(elem.getAttribute("MsgStructID"));
192                message.setOrderControl(elem.getAttribute("OrderControl"));
193                message.setEventDesc(elem.getAttribute("EventDesc"));
194                message.setIdentifier(elem.getAttribute("Identifier"));
195                message.setRole(elem.getAttribute("Role"));
196
197                Element md = getFirstElementByTagName("MetaData", elem);
198                if (md != null)
199                        message.setMetaData(parseMetaData(md));
200
201                message.setImpNote(getValueOfFirstElement("ImpNote", elem));
202                message.setDescription(getValueOfFirstElement("Description", elem));
203                message.setReference(getValueOfFirstElement("Reference", elem));
204
205                parseChildren(message, elem);
206                return message;
207        }
208
209        /** Parses metadata element */
210        private MetaData parseMetaData(Element elem) {
211                log.debug("ProfileParser.parseMetaData() has been called ... note that this method does nothing.");
212                return null;
213        }
214
215        /**
216         * Parses children (i.e. segment groups, segments) of a segment group or message profile
217         */
218        private void parseChildren(AbstractSegmentContainer parent, Element elem)
219                        throws ProfileException {
220                int childIndex = 1;
221                NodeList children = elem.getChildNodes();
222                for (int i = 0; i < children.getLength(); i++) {
223                        Node n = children.item(i);
224                        if (n.getNodeType() == Node.ELEMENT_NODE) {
225                                Element child = (Element) n;
226                                if (child.getNodeName().equalsIgnoreCase("SegGroup")) {
227                                        SegGroup group = parseSegmentGroupProfile(child);
228                                        parent.setChild(childIndex++, group);
229                                } else if (child.getNodeName().equalsIgnoreCase("Segment")) {
230                                        Seg segment = parseSegmentProfile(child);
231                                        parent.setChild(childIndex++, segment);
232                                }
233                        }
234                }
235        }
236
237        /** Parses a segment group profile */
238        private SegGroup parseSegmentGroupProfile(Element elem) throws ProfileException {
239                SegGroup group = new SegGroup();
240                log.debug("Parsing segment group profile: " + elem.getAttribute("Name"));
241
242                parseProfileStuctureData(group, elem);
243
244                parseChildren(group, elem);
245                return group;
246        }
247
248        /** Parses a segment profile */
249        private Seg parseSegmentProfile(Element elem) throws ProfileException {
250                Seg segment = new Seg();
251                log.debug("Parsing segment profile: " + elem.getAttribute("Name"));
252
253                parseProfileStuctureData(segment, elem);
254
255                int childIndex = 1;
256                NodeList children = elem.getChildNodes();
257                for (int i = 0; i < children.getLength(); i++) {
258                        Node n = children.item(i);
259                        if (n.getNodeType() == Node.ELEMENT_NODE) {
260                                Element child = (Element) n;
261                                if (child.getNodeName().equalsIgnoreCase("Field")) {
262                                        Field field = parseFieldProfile(child);
263                                        segment.setField(childIndex++, field);
264                                }
265                        }
266                }
267
268                return segment;
269        }
270
271        /** Parse common data in profile structure (eg SegGroup, Segment) */
272        private void parseProfileStuctureData(ProfileStructure struct, Element elem)
273                        throws ProfileException {
274                struct.setName(elem.getAttribute("Name"));
275                struct.setLongName(elem.getAttribute("LongName"));
276                struct.setUsage(elem.getAttribute("Usage"));
277                String min = elem.getAttribute("Min");
278                String max = elem.getAttribute("Max");
279                try {
280                        struct.setMin(Short.parseShort(min));
281                        if (max.indexOf('*') >= 0) {
282                                struct.setMax((short) -1);
283                        } else {
284                                struct.setMax(Short.parseShort(max));
285                        }
286                } catch (NumberFormatException e) {
287                        throw new ProfileException("Min and max must be short integers: " + min + ", " + max, e);
288                }
289
290                struct.setImpNote(getValueOfFirstElement("ImpNote", elem));
291                struct.setDescription(getValueOfFirstElement("Description", elem));
292                struct.setReference(getValueOfFirstElement("Reference", elem));
293                struct.setPredicate(getValueOfFirstElement("Predicate", elem));
294        }
295
296        /** Parses a field profile */
297        private Field parseFieldProfile(Element elem) throws ProfileException {
298                Field field = new Field();
299                log.debug("  Parsing field profile: " + elem.getAttribute("Name"));
300
301                field.setUsage(elem.getAttribute("Usage"));
302                String itemNo = elem.getAttribute("ItemNo");
303                String min = elem.getAttribute("Min");
304                String max = elem.getAttribute("Max");
305
306                try {
307                        if (itemNo.length() > 0) {
308                                field.setItemNo(Short.parseShort(itemNo));
309                        }
310                } catch (NumberFormatException e) {
311                        throw new ProfileException("Invalid ItemNo: " + itemNo + "( for name "
312                                        + elem.getAttribute("Name") + ")", e);
313                } // try-catch
314
315                try {
316                        field.setMin(Short.parseShort(min));
317                        if (max.indexOf('*') >= 0) {
318                                field.setMax((short) -1);
319                        } else {
320                                field.setMax(Short.parseShort(max));
321                        }
322                } catch (NumberFormatException e) {
323                        throw new ProfileException("Min and max must be short integers: " + min + ", " + max, e);
324                }
325
326                parseAbstractComponentData(field, elem);
327
328                int childIndex = 1;
329                NodeList children = elem.getChildNodes();
330                for (int i = 0; i < children.getLength(); i++) {
331                        Node n = children.item(i);
332                        if (n.getNodeType() == Node.ELEMENT_NODE) {
333                                Element child = (Element) n;
334                                if (child.getNodeName().equalsIgnoreCase("Component")) {
335                                        Component comp = (Component) parseComponentProfile(child, false);
336                                        field.setComponent(childIndex++, comp);
337                                }
338                        }
339                }
340
341                return field;
342        }
343
344        /** Parses a component profile */
345        private AbstractComponent<?> parseComponentProfile(Element elem, boolean isSubComponent)
346                        throws ProfileException {
347                AbstractComponent<?> comp = null;
348                if (isSubComponent) {
349                        log.debug("      Parsing subcomp profile: " + elem.getAttribute("Name"));
350                        comp = new SubComponent();
351                } else {
352                        log.debug("    Parsing comp profile: " + elem.getAttribute("Name"));
353                        comp = new Component();
354
355                        int childIndex = 1;
356                        NodeList children = elem.getChildNodes();
357                        for (int i = 0; i < children.getLength(); i++) {
358                                Node n = children.item(i);
359                                if (n.getNodeType() == Node.ELEMENT_NODE) {
360                                        Element child = (Element) n;
361                                        if (child.getNodeName().equalsIgnoreCase("SubComponent")) {
362                                                SubComponent subcomp = (SubComponent) parseComponentProfile(child, true);
363                                                ((Component) comp).setSubComponent(childIndex++, subcomp);
364                                        }
365                                }
366                        }
367                }
368
369                parseAbstractComponentData(comp, elem);
370
371                return comp;
372        }
373
374        /**
375         * Parses common features of AbstractComponents (ie field, component, subcomponent)
376         */
377        private void parseAbstractComponentData(AbstractComponent<?> comp, Element elem)
378                        throws ProfileException {
379                comp.setName(elem.getAttribute("Name"));
380                comp.setUsage(elem.getAttribute("Usage"));
381                comp.setDatatype(elem.getAttribute("Datatype"));
382                String length = elem.getAttribute("Length");
383                if (length != null && length.length() > 0) {
384                        try {
385                                comp.setLength(Long.parseLong(length));
386                        } catch (NumberFormatException e) {
387                                throw new ProfileException("Length must be a long integer: " + length, e);
388                        }
389                }
390                comp.setConstantValue(elem.getAttribute("ConstantValue"));
391                String table = elem.getAttribute("Table");
392                if (table != null && table.length() > 0) {
393                        try {
394                                comp.setTable(table);
395                        } catch (NumberFormatException e) {
396                                throw new ProfileException("Table must be a short integer: " + table, e);
397                        }
398                }
399
400                comp.setImpNote(getValueOfFirstElement("ImpNote", elem));
401                comp.setDescription(getValueOfFirstElement("Description", elem));
402                comp.setReference(getValueOfFirstElement("Reference", elem));
403                comp.setPredicate(getValueOfFirstElement("Predicate", elem));
404
405                int dataValIndex = 0;
406                NodeList children = elem.getChildNodes();
407                for (int i = 0; i < children.getLength(); i++) {
408                        Node n = children.item(i);
409                        if (n.getNodeType() == Node.ELEMENT_NODE) {
410                                Element child = (Element) n;
411                                if (child.getNodeName().equalsIgnoreCase("DataValues")) {
412                                        DataValue val = new DataValue();
413                                        val.setExValue(child.getAttribute("ExValue"));
414                                        comp.setDataValues(dataValIndex++, val);
415                                }
416                        }
417                }
418
419        }
420
421        /** Parses profile string into DOM document */
422        private Document parseIntoDOM(String profileString) throws ProfileException {
423                try {
424                        Document doc = XMLUtils.parse(profileString, true);
425                        if (alwaysValidate)
426                                XMLUtils.validate(doc, PROFILE_XSD, errorHandler);
427                        return doc;
428                } catch (Exception e) {
429                        throw new ProfileException("Exception parsing message profile: " + e.getMessage(), e);
430                }
431        }
432
433        /**
434         * Returns the first child element of the given parent that matches the given tag name. Returns
435         * null if no instance of the expected element is present.
436         */
437        private Element getFirstElementByTagName(String name, Element parent) {
438                NodeList nl = parent.getElementsByTagName(name);
439                Element ret = null;
440                if (nl.getLength() > 0) {
441                        ret = (Element) nl.item(0);
442                }
443                return ret;
444        }
445
446        /**
447         * Gets the result of getFirstElementByTagName() and returns the value of that element.
448         */
449        private String getValueOfFirstElement(String name, Element parent) throws ProfileException {
450                Element el = getFirstElementByTagName(name, parent);
451                String val = null;
452                if (el != null) {
453                        try {
454                                Node n = el.getFirstChild();
455                                if (n.getNodeType() == Node.TEXT_NODE) {
456                                        val = n.getNodeValue();
457                                }
458                        } catch (Exception e) {
459                                throw new ProfileException("Unable to get value of node " + name, e);
460                        }
461                }
462                return val;
463        }
464
465        public static void main(String args[]) {
466
467                if (args.length != 1) {
468                        System.out.println("Usage: ProfileParser profile_file");
469                        System.exit(1);
470                }
471
472                try {
473                        // File f = new
474                        // File("C:\\Documents and Settings\\bryan\\hapilocal\\hapi\\ca\\uhn\\hl7v2\\conf\\parser\\example_ack.xml");
475                        File f = new File(args[0]);
476                        @SuppressWarnings("resource")
477                        BufferedReader in = new BufferedReader(new FileReader(f));
478                        char[] cbuf = new char[(int) f.length()];
479                        in.read(cbuf, 0, (int) f.length());
480                        String xml = String.valueOf(cbuf);
481                        // System.out.println(xml);
482
483                        ProfileParser pp = new ProfileParser(true);
484                        pp.parse(xml);
485                } catch (Exception e) {
486                        e.printStackTrace();
487                }
488        }
489
490}