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.ByteArrayOutputStream;
042import java.io.IOException;
043import java.util.ArrayList;
044import java.util.LinkedList;
045
046import org.dcm4che3.data.Attributes;
047import org.dcm4che3.data.BulkData;
048import org.dcm4che3.data.ElementDictionary;
049import org.dcm4che3.data.Fragments;
050import org.dcm4che3.data.PersonName;
051import org.dcm4che3.data.Sequence;
052import org.dcm4che3.data.VR;
053import org.dcm4che3.util.Base64;
054import org.dcm4che3.util.ByteUtils;
055import org.dcm4che3.util.TagUtils;
056import org.xml.sax.SAXException;
057import org.xml.sax.helpers.DefaultHandler;
058
059/**
060 * @author Gunter Zeilinger <gunterze@gmail.com>
061 */
062public class ContentHandlerAdapter extends DefaultHandler {
063
064    private Attributes fmi;
065    private final boolean bigEndian;
066    private final boolean addBulkDataReferences;
067    private final LinkedList<Attributes> items = new LinkedList<Attributes>();
068    private final LinkedList<Sequence> seqs = new LinkedList<Sequence>();
069
070    private final ByteArrayOutputStream bout = new ByteArrayOutputStream(64);
071    private final char[] carry = new char[4];
072    private int carryLen;
073    private final StringBuilder sb = new StringBuilder(64);
074    private final ArrayList<String> values = new ArrayList<String>();
075    private PersonName pn;
076    private PersonName.Group pnGroup;
077    private int tag;
078    private String privateCreator;
079    private VR vr;
080    private BulkData bulkData;
081    private Fragments dataFragments;
082    private boolean processCharacters;
083    private boolean inlineBinary;
084
085    public ContentHandlerAdapter(Attributes attrs, boolean addBulkDataReferences) {
086        if (attrs == null)
087            throw new NullPointerException();
088        items.add(attrs);
089        bigEndian = attrs.bigEndian();
090        this.addBulkDataReferences = addBulkDataReferences;
091    }
092
093    public ContentHandlerAdapter(Attributes attrs) {
094        this(attrs, false);
095    }
096
097    public Attributes getFileMetaInformation() {
098        return fmi;
099    }
100
101    @Override
102    public void startElement(String uri, String localName, String qName,
103            org.xml.sax.Attributes atts) throws SAXException {
104        switch (qName.charAt(0)) {
105        case 'A':
106            if (qName.equals("Alphabetic"))
107                startPNGroup(PersonName.Group.Alphabetic);
108            break;
109        case 'B':
110            if (qName.equals("BulkData"))
111                bulkData(atts.getValue("uuid"), atts.getValue("uri"));
112            break;
113        case 'D':
114            if (qName.equals("DicomAttribute"))
115                startDicomAttribute(
116                        (int) Long.parseLong(atts.getValue("tag"), 16),
117                        atts.getValue("privateCreator"),
118                        atts.getValue("vr"));
119            else if (qName.equals("DataFragment"))
120                startDataFragment(Integer.parseInt(atts.getValue("number")));
121            break;
122        case 'F':
123            if (qName.equals("FamilyName"))
124                startText();
125            break;
126        case 'G':
127            if (qName.equals("GivenName"))
128                startText();
129            break;
130        case 'I':
131            if (qName.equals("Item"))
132                startItem(Integer.parseInt(atts.getValue("number")));
133            else if (qName.equals("InlineBinary"))
134                startInlineBinary();
135            else if (qName.equals("Ideographic"))
136                startPNGroup(PersonName.Group.Ideographic);
137            break;
138        case 'L':
139            if (qName.equals("Length"))
140                startText();
141            break;
142        case 'M':
143            if (qName.equals("MiddleName"))
144                startText();
145            break;
146        case 'N':
147            if (qName.equals("NamePrefix") || qName.equals("NameSuffix"))
148                startText();
149            break;
150        case 'O':
151            if (qName.equals("Offset"))
152                startText();
153            break;
154        case 'P':
155            if (qName.equals("PersonName")) {
156                startPersonName(Integer.parseInt(atts.getValue("number")));
157            } else if (qName.equals("Phonetic"))
158                startPNGroup(PersonName.Group.Phonetic);
159            break;
160        case 'T':
161            if (qName.equals("TransferSyntax"))
162                startText();
163            break;
164        case 'U':
165            if (qName.equals("URI"))
166                startText();
167            break;
168        case 'V':
169            if (qName.equals("Value")) {
170                startValue(Integer.parseInt(atts.getValue("number")));
171                startText();
172            }
173            break;
174        }
175   }
176
177    private void bulkData(String uuid, String uri) {
178        bulkData = new BulkData(uuid, uri, items.getLast().bigEndian());
179    }
180
181    private void startInlineBinary() {
182        processCharacters = true;
183        inlineBinary = true;
184        bout.reset();
185    }
186
187    private void startText() {
188        processCharacters = true;
189        inlineBinary = false;
190        sb.setLength(0);
191    }
192
193    private void startDicomAttribute(int tag, String privateCreator,
194            String vr) {
195        this.tag = tag;
196        this.privateCreator = privateCreator;
197        this.vr = vr != null ? VR.valueOf(vr)
198                             : ElementDictionary.vrOf(tag, privateCreator);
199        if (this.vr == VR.SQ)
200            seqs.add(items.getLast().newSequence(privateCreator, tag, 10));
201    }
202
203    private void startDataFragment(int number) {
204        if (dataFragments == null)
205            dataFragments = items.getLast()
206                    .newFragments(privateCreator, tag, vr,  10);
207        while (dataFragments.size() < number-1)
208            dataFragments.add(ByteUtils.EMPTY_BYTES);
209    }
210
211    private void startItem(int number) {
212        Sequence seq = seqs.getLast();
213        while (seq.size() < number-1)
214            seq.add(new Attributes(0));
215        Attributes item = new Attributes();
216        seq.add(item);
217        items.add(item);
218    }
219
220    private void startValue(int number) {
221        while (values.size() < number-1)
222            values.add(null);
223    }
224
225    private void startPersonName(int number) {
226        startValue(number);
227        pn = new PersonName();
228    }
229
230    private void startPNGroup(PersonName.Group pnGroup) {
231        this.pnGroup = pnGroup;
232    }
233
234    @Override
235    public void characters(char[] ch, int offset, int len)
236            throws SAXException {
237        if (processCharacters)
238            if (inlineBinary)
239                try {
240                    len = removeWhitespaces(ch, offset, len);
241                    if (carryLen != 0) {
242                        int copy = Math.min(4 - carryLen, len);
243                        System.arraycopy(ch, offset, carry, carryLen, copy);
244                        if ((carryLen += copy) < 4)
245                            return;
246
247                        Base64.decode(carry, 0, 4, bout);
248                        offset += copy;
249                        len -= copy;
250                    }
251                    if ((carryLen = len & 3) != 0) {
252                        len -= carryLen;
253                        System.arraycopy(ch, offset + len, carry, 0, carryLen);
254                    }
255                    Base64.decode(ch, offset, len, bout);
256                } catch (IOException e) {
257                    throw new RuntimeException(e);
258                }
259            else
260                sb.append(ch, offset, len);
261    }
262
263    private static int removeWhitespaces(char[] ch, int offset, int len) {
264        int ws = 0;
265        int srcPos = -1;
266        int destPos = -1;
267        int copy = 0;
268        for (int i = offset, end = offset + len; i < end; i++) {
269            switch (ch[i]) {
270                case ' ':
271                case '\t':
272                case '\r':
273                case '\n':
274                    if (copy > 0) {
275                        System.arraycopy(ch, srcPos, ch, destPos, copy);
276                        destPos += copy;
277                        copy = 0;
278                    } else if (destPos < 0) {
279                        destPos = i;
280                    }
281                    srcPos = i + 1;
282                    ws++;
283                    break;
284                default:
285                    if (ws > 0)
286                        copy++;
287            }
288        }
289        if (copy > 0)
290            System.arraycopy(ch, srcPos, ch, destPos, copy);
291        return len - ws;
292    }
293
294    @Override
295    public void endElement(String uri, String localName, String qName)
296            throws SAXException {
297        switch (qName.charAt(0)) {
298        case 'D':
299            if (qName.equals("DicomAttribute"))
300                endDicomAttribute();
301            else if (qName.equals("DataFragment"))
302                endDataFragment();
303            break;
304        case 'F':
305            if (qName.equals("FamilyName"))
306                endPNComponent(PersonName.Component.FamilyName);
307            break;
308        case 'G':
309            if (qName.equals("GivenName"))
310                endPNComponent(PersonName.Component.GivenName);
311            break;
312        case 'I':
313            if (qName.equals("Item"))
314                endItem();
315            else if (qName.equals("InlineBinary"))
316                endInlineBinary();
317            break;
318        case 'M':
319            if (qName.equals("MiddleName"))
320                endPNComponent(PersonName.Component.MiddleName);
321            break;
322        case 'N':
323            if (qName.equals("NamePrefix"))
324                endPNComponent(PersonName.Component.NamePrefix);
325            else if (qName.equals("NameSuffix"))
326                endPNComponent(PersonName.Component.NameSuffix);
327            break;
328        case 'P':
329            if (qName.equals("PersonName"))
330                endPersonName();
331            break;
332        case 'V':
333            if (qName.equals("Value")) {
334                endValue();
335            }
336            break;
337        }
338        processCharacters = false;
339    }
340
341    @Override
342    public void endDocument() throws SAXException {
343        if (fmi != null)
344            fmi.trimToSize();
345        items.getFirst().trimToSize();
346    }
347
348    private void endDataFragment() {
349        dataFragments.add(getBytes());
350    }
351
352    private void endDicomAttribute() {
353        if (vr == VR.SQ) {
354            seqs.removeLast().trimToSize();
355            return;
356        }
357        if (dataFragments != null) {
358            dataFragments.trimToSize();
359            dataFragments = null;
360            return;
361        }
362        Attributes attrs = items.getLast();
363        if (TagUtils.isFileMetaInformation(tag)) {
364            if (fmi == null)
365                fmi = new Attributes();
366            attrs = fmi;
367        }
368        if (bulkData != null) {
369            attrs.setValue(privateCreator, tag, vr,
370                    bulkData.hasFragments() ? bulkData.toFragments(privateCreator, tag, vr) : bulkData);
371            if (addBulkDataReferences)
372                attrs.getRoot().addBulkDataReference(privateCreator, tag, vr, bulkData, attrs.itemPointers());
373            bulkData = null;
374        } else if (inlineBinary) {
375            attrs.setBytes(privateCreator, tag, vr, getBytes());
376        } else {
377            attrs.setString(privateCreator, tag, vr, getStrings());
378        }
379    }
380
381    private void endItem() {
382        items.removeLast().trimToSize();
383        vr = VR.SQ;
384    }
385
386    private void endInlineBinary() throws SAXException {
387        if (carryLen != 0) {
388            // Attempting to end the inline binary section while we still have leftover characters in the carry
389            throw new SAXException("Inline binary data contained invalid number of characters");
390        }
391    }
392
393    private void endPersonName() {
394        values.add(pn.toString());
395        pn = null;
396    }
397
398    private void endValue() {
399        values.add(getString());
400    }
401
402    private void endPNComponent(PersonName.Component pnComp) {
403        pn.set(pnGroup, pnComp, getString());
404    }
405
406    private String getString() {
407        return sb.toString();
408    }
409
410    private byte[] getBytes() {
411        byte[] b = bout.toByteArray();
412        return bigEndian ? vr.toggleEndian(b, false) : b;
413    }
414
415    private String[] getStrings() {
416        try {
417            return values.toArray(new String[values.size()]);
418        } finally {;
419            values.clear();
420        }
421    }
422
423}