001/* ***** BEGIN LICENSE BLOCK *****
002 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
003 *
004 * The contents of this file are subject to the Mozilla Public License Version
005 * 1.1 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 * http://www.mozilla.org/MPL/
008 *
009 * Software distributed under the License is distributed on an "AS IS" basis,
010 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
011 * for the specific language governing rights and limitations under the
012 * License.
013 *
014 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in
015 * Java(TM), hosted at https://github.com/gunterze/dcm4che.
016 *
017 * The Initial Developer of the Original Code is
018 * Agfa Healthcare.
019 * Portions created by the Initial Developer are Copyright (C) 2011
020 * the Initial Developer. All Rights Reserved.
021 *
022 * Contributor(s):
023 * See @authors listed below
024 *
025 * Alternatively, the contents of this file may be used under the terms of
026 * either the GNU General Public License Version 2 or later (the "GPL"), or
027 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
028 * in which case the provisions of the GPL or the LGPL are applicable instead
029 * of those above. If you wish to allow use of your version of this file only
030 * under the terms of either the GPL or the LGPL, and not to allow others to
031 * use your version of this file under the terms of the MPL, indicate your
032 * decision by deleting the provisions above and replace them with the notice
033 * and other provisions required by the GPL or the LGPL. If you do not delete
034 * the provisions above, a recipient may use your version of this file under
035 * the terms of any one of the MPL, the GPL or the LGPL.
036 *
037 * ***** END LICENSE BLOCK ***** */
038
039package org.dcm4che3.io;
040
041import java.io.IOException;
042
043import org.dcm4che3.data.Attributes;
044import org.dcm4che3.data.BulkData;
045import org.dcm4che3.data.ElementDictionary;
046import org.dcm4che3.data.Fragments;
047import org.dcm4che3.data.PersonName;
048import org.dcm4che3.data.Sequence;
049import org.dcm4che3.data.SpecificCharacterSet;
050import org.dcm4che3.data.Tag;
051import org.dcm4che3.data.VR;
052import org.dcm4che3.data.Value;
053import org.dcm4che3.io.DicomInputStream.IncludeBulkData;
054import org.dcm4che3.util.Base64;
055import org.dcm4che3.util.ByteUtils;
056import org.dcm4che3.util.TagUtils;
057import org.xml.sax.ContentHandler;
058import org.xml.sax.SAXException;
059import org.xml.sax.helpers.AttributesImpl;
060
061/**
062 * @author Gunter Zeilinger <gunterze@gmail.com>
063 */
064public class SAXWriter implements DicomInputHandler {
065
066    private static final String NAMESPACE = "http://dicom.nema.org/PS3.19/models/NativeDICOM";
067    private static final int BASE64_CHUNK_LENGTH = 256 * 3;
068    private static final int BUFFER_LENGTH = 256 * 4;
069    
070    private boolean includeKeyword = true;
071    private String namespace = "";
072
073    private final ContentHandler ch;
074    private final AttributesImpl atts = new AttributesImpl();
075    private final char[] buffer = new char[BUFFER_LENGTH];
076
077    public SAXWriter(ContentHandler ch) {
078        this.ch = ch;
079    }
080
081    public final boolean isIncludeKeyword() {
082        return includeKeyword;
083    }
084
085    public final void setIncludeKeyword(boolean includeKeyword) {
086        this.includeKeyword = includeKeyword;
087    }
088
089    public final boolean isIncludeNamespaceDeclaration() {
090        return namespace == NAMESPACE;
091    }
092
093    public final void setIncludeNamespaceDeclaration(boolean includeNameSpaceDeclaration) {
094        this.namespace = includeNameSpaceDeclaration ? NAMESPACE : "";
095    }
096
097    public void write(Attributes attrs) throws SAXException {
098        startDocument();
099        writeItem(attrs);
100        endDocument();
101    }
102
103    private void writeItem(final Attributes item) throws SAXException {
104        final SpecificCharacterSet cs = item.getSpecificCharacterSet();
105        try {
106            item.accept(new Attributes.Visitor(){
107
108                @Override
109                public boolean visit(Attributes attrs, int tag, VR vr, Object value)
110                        throws Exception {
111                     writeAttribute(tag, vr, value, cs, item);
112                     return true;
113                }},
114                false);
115        } catch (SAXException e) {
116            throw e;
117        } catch (Exception e) {
118            throw new RuntimeException(e);
119        }
120    }
121
122
123    @Override
124    public void startDataset(DicomInputStream dis) throws IOException {
125        try {
126            startDocument();
127        } catch (SAXException e) {
128            throw new IOException(e);
129        }
130    }
131
132    @Override
133    public void endDataset(DicomInputStream dis) throws IOException {
134        try {
135            endDocument();
136        } catch (SAXException e) {
137            throw new IOException(e);
138        }
139    }
140
141    private void startDocument() throws SAXException {
142        ch.startDocument();
143        startElement("NativeDicomModel", "xml:space", "preserve");
144    }
145
146    private void endDocument() throws SAXException {
147        endElement("NativeDicomModel");
148        ch.endDocument();
149    }
150
151    private void startElement(String name, String attrName, int attrValue)
152            throws SAXException {
153        startElement(name, attrName, Integer.toString(attrValue));
154    }
155
156    private void startElement(String name, String attrName, String attrValue)
157            throws SAXException {
158        addAttribute(attrName, attrValue);
159        startElement(name);
160    }
161
162    private void startElement(String name) throws SAXException {
163        ch.startElement(namespace, name, name, atts);
164        atts.clear();
165    }
166
167    private void endElement(String name) throws SAXException {
168        ch.endElement(namespace, name, name);
169    }
170
171    private void addAttribute(String name, String value) {
172        atts.addAttribute(namespace, name, name, "NMTOKEN", value);
173    }
174
175    private void writeAttribute(int tag, VR vr, Object value,
176            SpecificCharacterSet cs, Attributes attrs) throws SAXException {
177        if (TagUtils.isGroupLength(tag) || TagUtils.isPrivateCreator(tag))
178            return;
179
180        String privateCreator = attrs.getPrivateCreator(tag);
181        addAttributes(tag, vr, privateCreator);
182        startElement("DicomAttribute");
183        if (value instanceof Value)
184            writeAttribute((Value) value, attrs.bigEndian());
185        else if (!vr.isInlineBinary()) {
186            writeValues(vr, value, attrs.bigEndian(),
187                    attrs.getSpecificCharacterSet(vr));
188        } else if (value instanceof byte[]) {
189            writeInlineBinary(attrs.bigEndian()
190                    ? vr.toggleEndian((byte[]) value, true)
191                    : (byte[]) value);
192        } else
193            throw new IllegalArgumentException("vr: " + vr + ", value class: "
194                    + value.getClass());
195        endElement("DicomAttribute");
196    }
197
198    private void writeAttribute(Value value, boolean bigEndian)
199            throws SAXException {
200        if (value.isEmpty())
201            return;
202
203        if (value instanceof Sequence) {
204            Sequence seq = (Sequence) value;
205            int number = 0;
206            for (Attributes item : seq) {
207                startElement("Item", "number", ++number);
208                writeItem(item);
209                endElement("Item");
210            }
211        } else if (value instanceof Fragments) {
212            Fragments frags = (Fragments) value;
213            if (frags.size() > 1 && frags.get(1) instanceof BulkData)
214                writeBulkData(BulkData.fromFragments(frags));
215            else {
216                int number = 0;
217                for (Object frag : frags) {
218                    ++number;
219                    if (frag instanceof Value && ((Value) frag).isEmpty())
220                        continue;
221                    startElement("DataFragment", "number", number);
222                    byte[] b = (byte[]) frag;
223                    if (bigEndian)
224                        frags.vr().toggleEndian(b, true);
225                    writeInlineBinary(b);
226                    endElement("DataFragment");
227                }
228            }
229        } else if (value instanceof BulkData) {
230            writeBulkData((BulkData) value);
231        }
232    }
233
234    @Override
235    public void readValue(DicomInputStream dis, Attributes attrs)
236            throws IOException {
237        int tag = dis.tag();
238        VR vr = dis.vr();
239        int len = dis.length();
240        if (TagUtils.isGroupLength(tag) || TagUtils.isPrivateCreator(tag)) {
241            dis.readValue(dis, attrs);
242        } else if (dis.getIncludeBulkData() == IncludeBulkData.NO
243                && dis.isBulkData(attrs)) {
244            if (len == -1)
245                dis.readValue(dis, attrs);
246            else
247                dis.skipFully(len);
248        } else try {
249            String privateCreator = attrs.getPrivateCreator(tag);
250            addAttributes(tag, vr, privateCreator);
251            startElement("DicomAttribute");
252            if (vr == VR.SQ || len == -1) {
253                dis.readValue(dis, attrs);
254                if (vr != VR.SQ && dis.getIncludeFragmentBulkData() == IncludeBulkData.URI) {
255                    writeBulkData(BulkData.fromFragments((Fragments) attrs.remove(privateCreator, tag)));
256                }
257            } else if (len > 0) {
258                if (dis.getIncludeBulkData() ==  IncludeBulkData.URI
259                        && dis.isBulkData(attrs)) {
260                    writeBulkData(dis.createBulkData());
261                } else {
262                    byte[] b = dis.readValue();
263                    if (tag == Tag.TransferSyntaxUID
264                            || tag == Tag.SpecificCharacterSet)
265                        attrs.setBytes(tag, vr, b);
266                    if (vr.isInlineBinary())
267                        writeInlineBinary(dis.bigEndian()
268                                ? vr.toggleEndian(b, false)
269                                : b);
270                    else
271                        writeValues(vr, b, dis.bigEndian(),
272                                attrs.getSpecificCharacterSet(vr));
273                 }
274            }
275            endElement("DicomAttribute");
276        } catch (SAXException e) {
277            throw new IOException(e);
278        }
279    }
280
281    private void addAttributes(int tag, VR vr, String privateCreator) {
282        if (privateCreator != null)
283            tag &= 0xffff00ff;
284        if (includeKeyword) {
285            String keyword = ElementDictionary.keywordOf(tag, privateCreator);
286            if (keyword != null && !keyword.isEmpty())
287                addAttribute("keyword", keyword);
288        }
289        addAttribute("tag", TagUtils.toHexString(tag));
290        if (privateCreator != null)
291            addAttribute("privateCreator", privateCreator);
292        addAttribute("vr", vr.name());
293    }
294
295    @Override
296    public void readValue(DicomInputStream dis, Sequence seq)
297            throws IOException {
298        try {
299            startElement("Item", "number", seq.size() + 1);
300            dis.readValue(dis, seq);
301            endElement("Item");
302        } catch (SAXException e) {
303            throw new IOException(e);
304        }
305    }
306
307    @Override
308    public void readValue(DicomInputStream dis, Fragments frags)
309            throws IOException {
310        int len = dis.length();
311        Object frag = ByteUtils.EMPTY_BYTES;
312        if (len > 0)
313            switch (dis.getIncludeFragmentBulkData()) {
314                case NO:
315                    dis.skipFully(len);
316                    return;
317                case URI:
318                    frag = dis.createBulkData();
319                    break;
320                case YES:
321                    byte[] b = dis.readValue();
322                    if (dis.bigEndian())
323                        frags.vr().toggleEndian(b, false);
324                    try {
325                        startElement("DataFragment", "number", frags.size() + 1);
326                        writeInlineBinary(b);
327                        endElement("DataFragment");
328                    } catch (SAXException e) {
329                        throw new IOException(e);
330                    }
331                    break;
332            }
333        frags.add(frag); // increment size
334    }
335
336    private void writeValues(VR vr, Object val, boolean bigEndian,
337            SpecificCharacterSet cs) throws SAXException {
338        if (vr.isStringType())
339            val = vr.toStrings(val, bigEndian, cs);
340        int vm = vr.vmOf(val);
341        for (int i = 0; i < vm; i++) {
342            String s = vr.toString(val, bigEndian, i, "");
343            addAttribute("number", Integer.toString(i + 1));
344            if (vr == VR.PN) {
345                PersonName pn = new PersonName(s, true);
346                startElement("PersonName");
347                writePNGroup("Alphabetic", pn, PersonName.Group.Alphabetic);
348                writePNGroup("Ideographic", pn, PersonName.Group.Ideographic);
349                writePNGroup("Phonetic", pn, PersonName.Group.Phonetic);
350                endElement("PersonName");
351            } else {
352                writeElement("Value", s);
353            }
354        }
355    }
356
357    private void writeInlineBinary(byte[] b) throws SAXException {
358        startElement("InlineBinary");
359        char[] buf = buffer;
360        for (int off = 0; off < b.length;) {
361            int len = Math.min(b.length - off, BASE64_CHUNK_LENGTH);
362            Base64.encode(b, off, len, buf, 0);
363            ch.characters(buf, 0, (len * 4 / 3 + 3) & ~3);
364            off += len;
365        }
366        endElement("InlineBinary");
367    }
368
369    private void writeBulkData(BulkData bulkData)
370            throws SAXException {
371        if (bulkData.uuid != null)
372            addAttribute("uuid", bulkData.uuid);
373        if (bulkData.uri != null)
374            addAttribute("uri", bulkData.uri);
375        startElement("BulkData");
376        endElement("BulkData");
377    }
378
379    private void writeElement(String qname, String s) throws SAXException {
380        if (s != null) {
381            startElement(qname); 
382            char[] buf = buffer;
383            for (int off = 0, totlen = s.length(); off < totlen;) {
384                int len = Math.min(totlen - off, buf.length);
385                s.getChars(off, off += len, buf, 0);
386                ch.characters(buf, 0, len);
387            }
388            endElement(qname);
389        }
390    }
391
392    private void writePNGroup(String qname, PersonName pn,
393            PersonName.Group group) throws SAXException {
394        if (pn.contains(group)) {
395            startElement(qname); 
396            writeElement("FamilyName",
397                    pn.get(group, PersonName.Component.FamilyName));
398            writeElement("GivenName",
399                    pn.get(group, PersonName.Component.GivenName));
400            writeElement("MiddleName",
401                    pn.get(group, PersonName.Component.MiddleName));
402            writeElement("NamePrefix",
403                    pn.get(group, PersonName.Component.NamePrefix));
404            writeElement("NameSuffix",
405                    pn.get(group, PersonName.Component.NameSuffix));
406            endElement(qname);
407        }
408    }
409
410}