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 Initial Developer of the Original Code is University Health Network. Copyright (C) 0102001. All Rights Reserved. 011 012Contributor(s): ______________________________________. 013 014Alternatively, the contents of this file may be used under the terms of the 015GNU General Public License (the �GPL�), in which case the provisions of the GPL are 016applicable instead of those above. If you wish to allow use of your version of this 017file only under the terms of the GPL and not to allow others to use your version 018of this file under the MPL, indicate your decision by deleting the provisions above 019and replace them with the notice and other provisions required by the GPL License. 020If you do not delete the provisions above, a recipient may use your version of 021this file under either the MPL or the GPL. 022 023*/ 024package ca.uhn.hl7v2.parser; 025 026import java.io.File; 027import java.io.FileReader; 028import java.util.ArrayList; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Set; 032 033import ca.uhn.hl7v2.DefaultHapiContext; 034import ca.uhn.hl7v2.model.GenericMessage; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037import org.w3c.dom.DOMException; 038import org.w3c.dom.Document; 039import org.w3c.dom.Element; 040import org.w3c.dom.Node; 041import org.w3c.dom.NodeList; 042 043import ca.uhn.hl7v2.HL7Exception; 044import ca.uhn.hl7v2.HapiContext; 045import ca.uhn.hl7v2.model.Group; 046import ca.uhn.hl7v2.model.Message; 047import ca.uhn.hl7v2.model.Segment; 048import ca.uhn.hl7v2.model.Structure; 049import ca.uhn.hl7v2.util.XMLUtils; 050import ca.uhn.hl7v2.validation.impl.NoValidation; 051import ca.uhn.hl7v2.validation.impl.ValidationContextFactory; 052 053/** 054 * <p>A default XMLParser. This class assigns segment elements (in an XML-encoded message) 055 * to Segment objects (in a Message object) using the name of a segment and the names 056 * of any groups in which the segment is nested. The names of group classes must correspond 057 * to the names of group elements (they must be identical except that a dot in the element 058 * name, following the message name, is replaced with an underscore, in order to consitute a 059 * valid class name). </p> 060 * <p>At the time of writing, the group names in the XML spec are changing. Many of the group 061 * names have been automatically generated based on the group contents. However, these automatic 062 * names are gradually being replaced with manually assigned names. This process is expected to 063 * be complete by November 2002. As a result, mismatches are likely. Messages could be 064 * transformed prior to parsing (using XSLT) as a work-around. Alternatively the group class names 065 * could be changed to reflect updates in the XML spec. Ultimately, HAPI group classes will be 066 * changed to correspond with the official group names, once these are all assigned. </p> 067 * 068 * @see ParserConfiguration for configuration options which may affect parser encoding and decoding behaviour 069 * @author Bryan Tripp 070 */ 071public class DefaultXMLParser extends XMLParser { 072 073 private static final Logger log = LoggerFactory.getLogger(DefaultXMLParser.class); 074 075 private static final Set<String> ourForceGroupNames; 076 077 static { 078 ourForceGroupNames = new HashSet<>(); 079 ourForceGroupNames.add("DIET"); 080 } 081 082 public DefaultXMLParser() { 083 super(); 084 } 085 086 public DefaultXMLParser(HapiContext context) { 087 super(context); 088 } 089 090 /** 091 * Creates a new instance of DefaultXMLParser 092 * 093 * @param theFactory custom factory to use for model class lookup 094 */ 095 public DefaultXMLParser(ModelClassFactory theFactory) { 096 super(theFactory); 097 } 098 099 /** 100 * <p>Creates an XML Document that corresponds to the given Message object. </p> 101 * <p>If you are implementing this method, you should create an XML Document, and insert XML Elements 102 * into it that correspond to the groups and segments that belong to the message type that your subclass 103 * of XMLParser supports. Then, for each segment in the message, call the method 104 * <code>encode(Segment segmentObject, Element segmentElement)</code> using the Element for 105 * that segment and the corresponding Segment object from the given Message.</p> 106 */ 107 public Document encodeDocument(Message source) throws HL7Exception { 108 String messageClassName = source.getClass().getName(); 109 String messageName = messageClassName.substring(messageClassName.lastIndexOf('.') + 1); 110 111 // Handle GenericMessages which will have an errant $ in their class name. 112 if (source instanceof GenericMessage) { 113 messageName = messageName.replaceAll("\\$", ""); 114 } 115 116 try { 117 Document doc = XMLUtils.emptyDocument(messageName); 118 encode(source, doc.getDocumentElement()); 119 return doc; 120 } catch (Exception e) { 121 throw new HL7Exception( 122 "Can't create XML document - " + e.getClass().getName(), e); 123 } 124 } 125 126 /** 127 * Copies data from a group object into the corresponding group element, creating any 128 * necessary child nodes. 129 */ 130 private void encode(Group groupObject, Element groupElement) throws HL7Exception { 131 String[] childNames = groupObject.getNames(); 132 String messageName = groupObject.getMessage().getName(); 133 134 try { 135 for (String name : childNames) { 136 Structure[] reps = groupObject.getAll(name); 137 for (Structure rep : reps) { 138 String elementName = makeGroupElementName(messageName, name); 139 Element childElement; 140 try { 141 childElement = groupElement.getOwnerDocument().createElementNS(NS, elementName); 142 } catch (DOMException e) { 143 throw new HL7Exception( 144 "Can't encode element " + elementName + " in group " + groupObject.getClass().getName(), e); 145 } 146 groupElement.appendChild(childElement); 147 if (rep instanceof Group) { 148 encode((Group) rep, childElement); 149 } 150 else if (rep instanceof Segment) { 151 encode((Segment) rep, childElement); 152 } 153 } 154 } 155 } catch (DOMException e) { 156 throw new HL7Exception( 157 "Can't encode group " + groupObject.getClass().getName(), e); 158 } 159 } 160 161 162 /** 163 * <p>Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message.</p> 164 * <p>The easiest way to implement this method for a particular message structure is as follows: 165 * <ol><li>Create an instance of the Message type you are going to handle with your subclass 166 * of XMLParser</li> 167 * <li>Go through the given Document and find the Elements that represent the top level of 168 * each message segment. </li> 169 * <li>For each of these segments, call <code>parse(Segment segmentObject, Element segmentElement)</code>, 170 * providing the appropriate Segment from your Message object, and the corresponding Element.</li></ol> 171 * At the end of this process, your Message object should be populated with data from the XML 172 * Document.</p> 173 * @throws HL7Exception if the message is not correctly formatted. 174 * @throws EncodingNotSupportedException if the message encoded 175 * is not supported by this parser. 176 */ 177 public Message parseDocument(Document xmlMessage, String version) throws HL7Exception { 178 179 assertNamespaceURI(xmlMessage.getDocumentElement().getNamespaceURI()); 180 181 Message message = instantiateMessage(xmlMessage.getDocumentElement().getLocalName(), version, true); 182 // Set parser before parsing the contents actually starts in order to respect 183 // the settings of the HapiContext 184 message.setParser(this); 185 parse(message, xmlMessage.getDocumentElement()); 186 return message; 187 } 188 189 /** 190 * Populates the given group object with data from the given group element, ignoring 191 * any unrecognized nodes. 192 */ 193 private void parse(Group groupObject, Element groupElement) throws HL7Exception { 194 String[] childNames = groupObject.getNames(); 195 String messageName = groupObject.getMessage().getName(); 196 197 NodeList allChildNodes = groupElement.getChildNodes(); 198 List<String> unparsedElementList = new ArrayList<>(); 199 for (int i = 0; i < allChildNodes.getLength(); i++) { 200 Node node = allChildNodes.item(i); 201 String name = node.getLocalName(); 202 if (node.getNodeType() == Node.ELEMENT_NODE && !unparsedElementList.contains(name)) { 203 assertNamespaceURI(node.getNamespaceURI()); 204 unparsedElementList.add(name); 205 } 206 } 207 208 //we're not too fussy about order here (all occurrences get parsed as repetitions) ... 209 for (String nextChildName : childNames) { 210 String childName = nextChildName; 211 if(groupObject.isGroup(nextChildName)) { 212 childName = makeGroupElementName(groupObject.getMessage().getName(), nextChildName); 213 } 214 unparsedElementList.remove(childName); 215 216 // 4 char segment names are second occurrences of a segment within a single message 217 // structure. e.g. the second PID segment in an A17 patient swap message is known 218 // to hapi's code represenation as PID2 219 if (nextChildName.length() == 4 && Character.isDigit(nextChildName.charAt(3))) { 220 log.trace("Skipping rep segment: {}", nextChildName); 221 } else { 222 parseReps(groupElement, groupObject, messageName, nextChildName, nextChildName); 223 } 224 } 225 226 for (String segName : unparsedElementList) { 227 String segIndexName = groupObject.addNonstandardSegment(segName); 228 parseReps(groupElement, groupObject, messageName, segName, segIndexName); 229 } 230 } 231 232 //param childIndexName may have an integer on the end if >1 sibling with same name (e.g. NTE2) 233 private void parseReps(Element groupElement, Group groupObject, 234 String messageName, String childName, String childIndexName) throws HL7Exception { 235 236 String groupName = makeGroupElementName(messageName, childName); 237 List<Element> reps = getChildElementsByTagName(groupElement, groupName); 238 log.trace("# of elements matching {}: {}", groupName, reps.size()); 239 240 if (groupObject.isRepeating(childIndexName)) { 241 for (int i = 0; i < reps.size(); i++) { 242 parseRep(reps.get(i), groupObject.get(childIndexName, i)); 243 } 244 } else { 245 if (reps.size() > 0) { 246 parseRep(reps.get(0), groupObject.get(childIndexName, 0)); 247 } 248 249// if (reps.size() > 1) { 250// String newIndexName = groupObject.addNonstandardSegment(childName); 251// for (int i = 1; i < reps.size(); i++) { 252// parseRep((Element) reps.get(i), groupObject.get(newIndexName, i-1)); 253// } 254// } 255 if (reps.size() > 1) { 256 String newIndexName; 257 int i=1; 258 try { 259 for (i = 1; i < reps.size(); i++) { 260 newIndexName = childName+(i+1); 261 Structure st = groupObject.get(newIndexName); 262 parseRep(reps.get(i), st); 263 } 264 } catch(Throwable t) { 265 log.info("Issue Parsing: " + t); 266 newIndexName = groupObject.addNonstandardSegment(childName); 267 for (int j = i; j < reps.size(); j++) { 268 parseRep(reps.get(j), groupObject.get(newIndexName, j-i)); 269 } 270 } 271 } 272 273 } 274 } 275 276 private void parseRep(Element theElem, Structure theObj) throws HL7Exception { 277 if (theObj instanceof Group) { 278 parse((Group) theObj, theElem); 279 } 280 else if (theObj instanceof Segment) { 281 parse((Segment) theObj, theElem); 282 } 283 log.trace("Parsed element: {}", theElem.getNodeName()); 284 } 285 286 //includes direct children only 287 private List<Element> getChildElementsByTagName(Element theElement, String theName) throws HL7Exception { 288 List<Element> result = new ArrayList<>(10); 289 NodeList children = theElement.getChildNodes(); 290 291 for (int i = 0; i < children.getLength(); i++) { 292 Node child = children.item(i); 293 if (child.getNodeType() == Node.ELEMENT_NODE && child.getLocalName().equals(theName)) { 294 assertNamespaceURI(child.getNamespaceURI()); 295 result.add((Element)child); 296 } 297 } 298 299 return result; 300 } 301 302 /** 303 * Given the name of a group element in an XML message, returns the corresponding 304 * group class name. This name is identical except in order to be a valid class 305 * name, the dot character immediately following the message name is replaced with 306 * an underscore. For example, there is a group element called ADT_A01.INSURANCE and the 307 * corresponding group Class is called ADT_A01_INSURANCE. 308 */ 309// protected static String makeGroupClassName(String elementName) { 310// return elementName.replace('.', '_'); 311// } 312 313 /** 314 * Given the name of a message and a Group class, returns the corresponding group element name in an 315 * XML-encoded message. This is the message name and group name separated by a dot. For example, 316 * ADT_A01.INSURANCE. 317 * 318 * If it looks like a segment name (i.e. has 3 characters), no change is made. 319 */ 320 protected static String makeGroupElementName(String messageName, String className) { 321 String ret; 322 323 if (className.length() > 4 || ourForceGroupNames.contains(className)) { 324 ret = messageName + 325 '.' + 326 className; 327 } else if (className.length() == 4) { 328 // It is not clear why this case is needed.. We should figure out 329 // why it was added, since removing it or optimizing its use would 330 // prevent the need for "ourForGroupNames" above 331 ret = className.substring(0,3); 332 } else { 333 ret = className; 334 } 335 336 return ret; 337 } 338 339 /** Test harness */ 340 public static void main(String[] args) { 341 if (args.length != 1) { 342 System.out.println("Usage: DefaultXMLParser pipe_encoded_file"); 343 System.exit(1); 344 } 345 346 //read and parse message from file 347 try { 348 File messageFile = new File(args[0]); 349 long fileLength = messageFile.length(); 350 FileReader r = new FileReader(messageFile); 351 char[] cbuf = new char[(int) fileLength]; 352 System.out.println("Reading message file ... " + r.read(cbuf) + " of " + fileLength + " chars"); 353 r.close(); 354 String messString = String.valueOf(cbuf); 355 356 Parser inParser = null; 357 Parser outParser = null; 358 PipeParser pp = new PipeParser(); 359 ca.uhn.hl7v2.parser.XMLParser xp = new DefaultXMLParser(); 360 System.out.println("Encoding: " + pp.getEncoding(messString)); 361 if (pp.getEncoding(messString) != null) { 362 inParser = pp; 363 outParser = xp; 364 } 365 else if (xp.getEncoding(messString) != null) { 366 inParser = xp; 367 outParser = pp; 368 } 369 370 Message mess = inParser.parse(messString); 371 System.out.println("Got message of type " + mess.getClass().getName()); 372 373 String otherEncoding = outParser.encode(mess); 374 System.out.println(otherEncoding); 375 } 376 catch (Exception e) { 377 e.printStackTrace(); 378 } 379 } 380 381 /** 382 * {@inheritDoc} 383 */ 384 @Override 385 public void parse(Message theMessage, String theString) throws HL7Exception { 386 theMessage.setParser(this); 387 Document doc = parseStringIntoDocument(theString); 388 parse(theMessage, doc.getDocumentElement()); 389 390 applySuperStructureName(theMessage); 391 } 392 393 /** 394 * Convenience factory method which returns an instance that has a 395 * {@link NoValidation NoValidation validation context}. 396 */ 397 public static XMLParser getInstanceWithNoValidation() { 398 HapiContext context = new DefaultHapiContext(ValidationContextFactory.noValidation()); 399 return context.getXMLParser(); 400 } 401 402 403}