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