001package org.hl7.fhir.utilities.xhtml;
002
003/*-
004 * #%L
005 * org.hl7.fhir.utilities
006 * %%
007 * Copyright (C) 2014 - 2019 Health Level 7
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023
024import java.io.IOException;
025
026import org.hl7.fhir.exceptions.FHIRException;
027import org.hl7.fhir.utilities.Utilities;
028import org.hl7.fhir.utilities.xml.IXMLWriter;
029import org.hl7.fhir.utilities.xml.XMLUtil;
030import org.w3c.dom.Element;
031import org.w3c.dom.Node;
032
033public class CDANarrativeFormat {
034
035  /** 
036   * for a CDA narrative, return the matching XHTML. 
037   * 
038   * For further information, see http://wiki.hl7.org/index.php?title=CDA_Narrative_to_html_mapping
039   * 
040   * @param ed
041   * @return
042   * @throws FHIRException
043   */
044  public XhtmlNode convert(Element ed) throws FHIRException {
045    XhtmlNode div = new XhtmlNode(NodeType.Element, "div");
046    div.setAttribute("xmlns", XhtmlNode.XMLNS);
047    processAttributes(ed, div, "ID", "language", "styleCode");
048    processChildren(ed, div);
049    return div;
050  }
051
052  private void processChildren(Element ed, XhtmlNode x) throws FHIRException {
053    for (Node n : XMLUtil.children(ed)) 
054      processChildNode(n, x);
055  }
056  
057  private void processChildNode(Node n, XhtmlNode xn) throws FHIRException {
058    switch (n.getNodeType()) {
059    case Node.ATTRIBUTE_NODE: 
060    case Node.CDATA_SECTION_NODE:
061    case Node.DOCUMENT_FRAGMENT_NODE: 
062    case Node.DOCUMENT_TYPE_NODE: 
063    case Node.DOCUMENT_NODE: 
064    case Node.ENTITY_NODE: 
065    case Node.PROCESSING_INSTRUCTION_NODE:
066    case Node.NOTATION_NODE:
067      return;
068    case Node.ENTITY_REFERENCE_NODE: 
069      throw new Error("Not handled yet");
070    case Node.COMMENT_NODE: 
071      xn.addComment(n.getTextContent());
072      return;
073    case Node.TEXT_NODE: 
074      if (!Utilities.isWhitespace(n.getTextContent()))
075        xn.addText(n.getTextContent());
076      return;
077    case Node.ELEMENT_NODE:
078      Element e = (Element) n;
079      if (n.getNodeName().equals("br"))
080        processBreak(e, xn);
081      else if (n.getNodeName().equals("caption"))
082        processCaption(e, xn);
083      else if (n.getNodeName().equals("col"))
084        processCol(e, xn);
085      else if (n.getNodeName().equals("colgroup"))
086        processColGroup(e, xn);
087      else if (n.getNodeName().equals("content"))
088        processContent(e, xn);
089      else if (n.getNodeName().equals("footnote"))
090        processFootNote(e, xn);
091      else if (n.getNodeName().equals("footnoteRef"))
092        processFootNodeRef(e, xn);
093      else if (n.getNodeName().equals("item"))
094        processItem(e, xn);
095      else if (n.getNodeName().equals("linkHtml"))
096        processlinkHtml(e, xn);
097      else if (n.getNodeName().equals("list"))
098        processList(e, xn);
099      else if (n.getNodeName().equals("paragraph"))
100        processParagraph(e, xn);
101      else if (n.getNodeName().equals("renderMultiMedia"))
102        processRenderMultiMedia(e, xn);
103      else if (n.getNodeName().equals("sub"))
104        processSub(e, xn);
105      else if (n.getNodeName().equals("sup"))
106        processSup(e, xn);
107      else if (n.getNodeName().equals("table"))
108        processTable(e, xn);
109      else if (n.getNodeName().equals("tbody"))
110        processTBody(e, xn);
111      else if (n.getNodeName().equals("td"))
112        processTd(e, xn);
113      else if (n.getNodeName().equals("tfoot"))
114        processTFoot(e, xn);
115      else if (n.getNodeName().equals("th"))
116        processTh(e, xn);
117      else if (n.getNodeName().equals("thead"))
118        processTHead(e, xn);
119      else if (n.getNodeName().equals("tr"))
120        processTr(e, xn);
121      else
122        throw new FHIRException("Unknown element "+n.getNodeName());
123    }
124  }
125
126  private void processBreak(Element e, XhtmlNode xn) {
127    xn.addTag("br");
128  }
129
130  private void processCaption(Element e, XhtmlNode xn) throws FHIRException {
131    XhtmlNode xc = xn.addTag("h2");
132    processAttributes(e, xc, "ID", "language", "styleCode");
133    processChildren(e, xc);
134  }
135
136  private void processCol(Element e, XhtmlNode xn) throws FHIRException {
137    XhtmlNode xc = xn.addTag("col");
138    processAttributes(e, xc, "ID", "language", "styleCode", "span", "width", "align", "char", "charoff", "valign");
139    processChildren(e, xc);
140  }
141
142  private void processColGroup(Element e, XhtmlNode xn) throws FHIRException {
143    XhtmlNode xc = xn.addTag("colgroup");
144    processAttributes(e, xc, "ID", "language", "styleCode", "span", "width", "align", "char", "charoff", "valign");
145    processChildren(e, xc);
146  }
147
148  private void processContent(Element e, XhtmlNode xn) throws FHIRException {
149    XhtmlNode xc = xn.addTag("span");
150    processAttributes(e, xc, "ID", "language", "styleCode");
151    // todo: do something with revised..., "revised"
152    processChildren(e, xc);
153  }
154
155  private void processFootNote(Element e, XhtmlNode xn) {
156    XhtmlNode xc = xn.addTag("tfoot");
157    processAttributes(e, xc, "ID", "language", "styleCode", "align", "char", "charoff", "valign");
158    processChildren(e, xc);
159  }
160
161  private void processFootNodeRef(Element e, XhtmlNode xn) {
162    throw new Error("element "+e.getNodeName()+" not handled yet");
163  }
164
165  private void processItem(Element e, XhtmlNode xn) throws FHIRException {
166    XhtmlNode xc = xn.addTag("li");
167    processAttributes(e, xc, "ID", "language", "styleCode");
168    processChildren(e, xc);
169  }
170
171  private void processlinkHtml(Element e, XhtmlNode xn) throws FHIRException {
172    XhtmlNode xc = xn.addTag("a");
173    processAttributes(e, xc, "name", "href", "rel", "rev", "title", "ID", "language", "styleCode");
174    processChildren(e, xc);
175  }
176
177  private void processList(Element e, XhtmlNode xn) throws FHIRException {
178    String lt = e.getAttribute("listType");
179    XhtmlNode xc = xn.addTag("ordered".equals(lt) ? "ol" : "ul");
180    processAttributes(e, xc, "ID", "language", "styleCode");
181    processChildren(e, xc);
182  }
183
184  private void processParagraph(Element e, XhtmlNode xn) throws FHIRException {
185    XhtmlNode xc = xn.addTag("p");
186    processAttributes(e, xc, "ID", "language", "styleCode");
187    processChildren(e, xc);
188  }
189
190  private void processRenderMultiMedia(Element e, XhtmlNode xn) throws FHIRException {
191    XhtmlNode xc = xn.addTag("img");
192    String v = e.getAttribute("referencedObject");
193    xc.attribute("src", v);
194    processAttributes(e, xc, "ID", "language", "styleCode");
195    processChildren(e, xc);
196  }
197
198  private void processSub(Element e, XhtmlNode xn) throws FHIRException {
199    XhtmlNode xc = xn.addTag("sub");
200    processChildren(e, xc);
201  }
202
203  private void processSup(Element e, XhtmlNode xn) throws FHIRException {
204    XhtmlNode xc = xn.addTag("sup");
205    processChildren(e, xc);
206  }
207
208  private void processTable(Element e, XhtmlNode xn) throws FHIRException {
209    XhtmlNode xc = xn.addTag("table");
210    processAttributes(e, xc, "ID", "language", "styleCode", "summary", "width", "border", "frame", "rules", "cellspacing", "cellpadding");
211    processChildren(e, xc);
212  }
213
214  private void processTBody(Element e, XhtmlNode xn) throws FHIRException {
215    XhtmlNode xc = xn.addTag("tbody");
216    processAttributes(e, xc, "ID", "language", "styleCode", "align", "char", "charoff", "valign");
217    processChildren(e, xc);
218  }
219
220  private void processTd(Element e, XhtmlNode xn) throws FHIRException {
221    XhtmlNode xc = xn.addTag("td");
222    processAttributes(e, xc, "ID", "language", "styleCode", "abbr", "axis", "headers", "scope", "rowspan", "colspan", "align", "char", "charoff", "valign");
223    processChildren(e, xc);
224  }
225
226  private void processTFoot(Element e, XhtmlNode xn) {
227    XhtmlNode xc = xn.addTag("tfoot");
228    processAttributes(e, xc, "ID", "language", "styleCode", "align", "char", "charoff", "valign");
229    processChildren(e, xc);
230  }
231
232  private void processTh(Element e, XhtmlNode xn) throws FHIRException {
233    XhtmlNode xc = xn.addTag("th");
234    processAttributes(e, xc, "ID", "language", "styleCode", "abbr", "axis", "headers", "scope", "rowspan", "colspan", "align", "char", "charoff", "valign");
235    processChildren(e, xc);
236  }
237
238  private void processTHead(Element e, XhtmlNode xn) throws FHIRException {
239    XhtmlNode xc = xn.addTag("thead");
240    processAttributes(e, xc, "ID", "language", "styleCode", "align", "char", "charoff", "valign");
241    processChildren(e, xc);
242  }
243
244  private void processTr(Element e, XhtmlNode xn) throws FHIRException {
245    XhtmlNode xc = xn.addTag("tr");
246    processAttributes(e, xc, "ID", "language", "styleCode", "align", "char", "charoff", "valign");
247    processChildren(e, xc);
248  }
249
250  private void processAttributes(Element element, XhtmlNode xn, String... names) {
251    for (String n : names) {
252      if (element.hasAttribute(n)) {
253        String v = element.getAttribute(n);
254        switch(n) {
255          case "ID":
256            xn.attribute("id", v);
257            break;
258          case "styleCode":
259            String style = v;
260            switch(v) {
261              // according Table 15.2 CSS rendering, The CDAtm book, Keith W. Boone
262              case "Bold":
263                style = "font-weight: bold";
264                break;
265              case "Underline":
266                style = "text-decoration: underline";
267                break;
268              case "Italics":
269                style = "font-style: italic";
270                break;
271              case "Emphasis":
272                style = "font-weight: small-caps";
273                break;
274              case "Lrule":
275                style = "border-left: 1px";
276                break;
277              case "Rrule":
278                style = "border-right: 1px";
279                break;
280              case "Toprule":
281                style = "border-top: 1px";
282                break;
283              case "Botrule":
284                style = "border-bottom: 1px";
285                break;
286              case "Arabic":
287                style = "list-style-type: decimal";
288                break;
289              case "LittleRoman":
290                style = "list-style-type: lower-roman";
291                break;
292              case "BigRoman":
293                style = "list-style-type: upper-roman";
294                break;
295              case "LittleAlpha":
296                style = "list-style-type: lower-alpha";
297                break;
298              case "BigAlpha":
299                style = "list-style-type: upper-alpha";
300                break;
301              case "Disc":
302                style = "list-style-type: disc";
303                break;
304              case "Circle":
305                style = "list-style-type: circle";
306                break;
307              case "Square":
308                style = "list-style-type: square";
309                break;
310            }
311            xn.attribute("style", style);
312            break;
313          default:
314            xn.attribute(n, v);
315        }
316      }
317    }
318  }
319
320  /**
321   * For XHTML return the matching CDA narrative. This is only guaranteed to work for XML produced from CDA, but will try whatever
322   * @param node
323   * @return
324   * @throws IOException 
325   * @throws FHIRException 
326   */
327  public void convert(IXMLWriter xml, XhtmlNode div) throws IOException, FHIRException {
328    processAttributes(div, xml, "ID", "language", "styleCode");
329    xml.enter("text");
330    processChildren(xml, div);
331    xml.exit("text");
332  }
333
334  private void processChildren(IXMLWriter xml, XhtmlNode x) throws IOException, FHIRException {
335    for (XhtmlNode n : x.getChildNodes()) 
336      processChildNode(xml, n);
337  }
338  
339  private void processChildNode(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
340    switch (n.getNodeType()) {
341    case DocType: 
342    case Document: 
343    case Instruction: 
344      return;
345    case Comment: 
346      xml.comment(n.getContent(), true);
347      return;
348    case Text: 
349      xml.text(n.getContent());
350      return;
351    case Element:
352      if (n.getName().equals("br"))
353        processBreak(xml, n);
354      else if (n.getName().equals("h2"))
355        processCaption(xml, n);
356      else if (n.getName().equals("col"))
357        processCol(xml, n);
358      else if (n.getName().equals("colgroup"))
359        processColGroup(xml, n);
360      else if (n.getName().equals("span"))
361        processContent(xml, n);
362      else if (n.getName().equals("footnote"))
363        processFootNote(xml, n);
364      else if (n.getName().equals("footnoteRef"))
365        processFootNodeRef(xml, n);
366      else if (n.getName().equals("li"))
367        processItem(xml, n);
368      else if (n.getName().equals("linkHtml"))
369        processlinkHtml(xml, n);
370      else if (n.getName().equals("ul") || n.getName().equals("ol"))
371        processList(xml, n);
372      else if (n.getName().equals("p"))
373        processParagraph(xml, n);
374      else if (n.getName().equals("img"))
375        processRenderMultiMedia(xml, n);
376      else if (n.getName().equals("sub"))
377        processSub(xml, n);
378      else if (n.getName().equals("sup"))
379        processSup(xml, n);
380      else if (n.getName().equals("table"))
381        processTable(xml, n);
382      else if (n.getName().equals("tbody"))
383        processTBody(xml, n);
384      else if (n.getName().equals("td"))
385        processTd(xml, n);
386      else if (n.getName().equals("tfoot"))
387        processTFoot(xml, n);
388      else if (n.getName().equals("th"))
389        processTh(xml, n);
390      else if (n.getName().equals("thead"))
391        processTHead(xml, n);
392      else if (n.getName().equals("tr"))
393        processTr(xml, n);
394      else
395        throw new FHIRException("Unknown element "+n.getName());
396    }
397  }
398
399  private void processBreak(IXMLWriter xml, XhtmlNode n) throws IOException {
400    xml.element("br");
401  }
402
403  private void processCaption(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
404    processAttributes(n, xml, "id", "language", "styleCode");
405    xml.enter("caption");
406    processChildren(xml, n);
407    xml.exit("caption");
408  }
409
410  private void processCol(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
411    processAttributes(n, xml, "id", "language", "styleCode", "span", "width", "align", "char", "charoff", "valign");
412    xml.enter("col");
413    processChildren(xml, n);
414    xml.exit("col");
415  }
416
417  private void processColGroup(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
418    processAttributes(n, xml, "id", "language", "styleCode", "span", "width", "align", "char", "charoff", "valign");
419    xml.enter("colgroup");
420    processChildren(xml, n);
421    xml.exit("colgroup");
422  }
423
424  private void processContent(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
425    processAttributes(n, xml, "id", "language", "styleCode");
426    xml.enter("content");
427    // todo: do something with revised..., "revised"
428    processChildren(xml, n);
429    xml.exit("content");
430  }
431
432  private void processFootNote(IXMLWriter xml, XhtmlNode n) {
433    throw new Error("element "+n.getName()+" not handled yet");
434  }
435
436  private void processFootNodeRef(IXMLWriter xml, XhtmlNode n) {
437    throw new Error("element "+n.getName()+" not handled yet");
438  }
439
440  private void processItem(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
441    processAttributes(n, xml, "id", "language", "styleCode");
442    xml.enter("item");
443    processChildren(xml, n);
444    xml.exit("item");
445  }
446
447  private void processlinkHtml(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
448    String v = n.getAttribute("src");
449    xml.attribute("referencedObject", v);
450    processAttributes(n, xml, "name", "href", "rel", "rev", "title", "id", "language", "styleCode");
451    xml.enter("linkHtml");
452    processChildren(xml, n);
453    xml.exit("linkHtml");
454  }
455
456  private void processList(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
457    if (n.getName().equals("ol"))
458      xml.attribute("listType", "ordered");
459    else
460      xml.attribute("listType", "unordered");
461    processAttributes(n, xml, "id", "language", "styleCode");
462    xml.enter("list");
463    processChildren(xml, n);
464    xml.exit("list");
465  }
466
467  private void processParagraph(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
468    processAttributes(n, xml, "id", "language", "styleCode");
469    xml.enter("paragraph");
470    processChildren(xml, n);
471    xml.exit("paragraph");
472  }
473
474  private void processRenderMultiMedia(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
475    String v = n.getAttribute("src");
476    xml.attribute("referencedObject", v);
477    processAttributes(n, xml, "id", "language", "styleCode");
478    xml.enter("renderMultiMedia");
479    processChildren(xml, n);
480    xml.exit("renderMultiMedia");
481  }
482
483  private void processSub(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
484    xml.enter("sub");
485    processChildren(xml, n);
486    xml.exit("sub");
487  }
488
489  private void processSup(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
490    xml.enter("sup");
491    processChildren(xml, n);
492    xml.exit("sup");
493  }
494
495  private void processTable(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
496    processAttributes(n, xml, "id", "language", "styleCode", "summary", "width", "border", "frame", "rules", "cellspacing", "cellpadding");
497    xml.enter("table");
498    processChildren(xml, n);
499    xml.exit("table");
500  }
501
502  private void processTBody(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
503    processAttributes(n, xml, "id", "language", "styleCode", "align", "char", "charoff", "valign");
504    xml.enter("tbody");
505    processChildren(xml, n);
506    xml.exit("tbody");
507  }
508
509  private void processTd(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
510    processAttributes(n, xml, "id", "language", "styleCode", "abbr", "axis", "headers", "scope", "rowspan", "colspan", "align", "char", "charoff", "valign");
511    xml.enter("td");
512    processChildren(xml, n);
513    xml.exit("td");
514  }
515
516  private void processTFoot(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
517    processAttributes(n, xml, "id", "language", "styleCode", "align", "char", "charoff", "valign");
518    xml.enter("tfoot");
519    processChildren(xml, n);
520    xml.exit("tfoot");
521  }
522
523  private void processTh(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
524    processAttributes(n, xml, "id", "language", "styleCode", "abbr", "axis", "headers", "scope", "rowspan", "colspan", "align", "char", "charoff", "valign");
525    xml.enter("th");
526    processChildren(xml, n);
527    xml.exit("th");
528  }
529
530  private void processTHead(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
531    processAttributes(n, xml, "id", "language", "styleCode", "align", "char", "charoff", "valign");
532    xml.enter("thead");
533    processChildren(xml, n);
534    xml.exit("thead");
535  }
536
537  private void processTr(IXMLWriter xml, XhtmlNode n) throws IOException, FHIRException {
538    processAttributes(n, xml, "id", "language", "styleCode", "align", "char", "charoff", "valign");
539    xml.enter("tr");
540    processChildren(xml, n);
541    xml.exit("tr");
542  }
543
544  private void processAttributes(XhtmlNode xn, IXMLWriter xml, String... names) throws IOException {
545    for (String n : names) {
546      if (xn.hasAttribute(n)) {
547        String v = xn.getAttribute(n);
548        switch(n) {
549          case "id":
550            xml.attribute("ID", v);
551            break;
552          case "style":
553            String style = v;
554            switch(v) {
555              // according Table 15.2 CSS rendering, The CDAtm book, Keith W. Boone, will not cover everything, just reverse of processAttributes
556              case "font-weight: bold":
557                style = "Bold";
558                break;
559              case "text-decoration: underline":
560                style = "Underline";
561                break;
562              case "font-style: italic":
563                style = "Italics";
564                break;
565              case "font-weight: small-caps":
566                style = "Emphasis";
567                break;
568              case "border-left: 1px":
569                style = "Lrule";
570                break;
571              case "border-right: 1px":
572                style = "Rrule";
573                break;
574              case "border-top: 1px":
575                style = "Toprule";
576                break;
577              case "border-bottom: 1px":
578                style = "Botrule";
579                break;
580              case "List-style-type: decimal":
581                style = "Arabic";
582                break;
583              case "list-style-type: lower-roman":
584                style = "LittleRoman";
585                break;
586              case "list-style-type: upper-roman":
587                style = "BigRoman";
588                break;
589              case "list-style-type: lower-alpha":
590                style = "LittleAlpha";
591                break;
592              case "list-style-type: upper-alpha":
593                style = "BigAlpha";
594                break;
595              case "list-style-type: disc":
596                style = "Disc";
597                break;
598              case "list-style-type: circle":
599                style = "Circle";
600                break;
601              case "list-style-type: square":
602                style = "Square";
603                break;
604            }
605            xml.attribute("styleCode", style);
606            break;
607          default:
608            xml.attribute(n, v);
609        }
610      }
611    }
612  }
613
614
615}