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.data;
040
041import java.io.File;
042import java.io.FileInputStream;
043import java.io.IOException;
044import java.io.InputStream;
045import java.io.ObjectInputStream;
046import java.io.ObjectOutputStream;
047import java.net.URI;
048import java.net.URISyntaxException;
049import java.net.URL;
050
051import org.dcm4che3.io.DicomEncodingOptions;
052import org.dcm4che3.io.DicomOutputStream;
053import org.dcm4che3.util.ByteUtils;
054import org.dcm4che3.util.StreamUtils;
055import org.dcm4che3.util.StringUtils;
056
057/**
058 * @author Gunter Zeilinger <gunterze@gmail.com>
059 */
060public class BulkData implements Value {
061
062    public static final int MAGIC_LEN = 0xfbfb;
063
064    public final String uri;
065    public final String uuid;
066    public final boolean bigEndian;
067
068    // derived fields, not considered for equals/hashCode:
069    private int uriPathEnd;
070    private long offset;
071    private int length = -1;
072    private long[] offsets;
073    private int[] lengths;
074
075    public BulkData(String uuid, String uri, boolean bigEndian) {
076        if (uri != null) {
077            if (uuid != null)
078                throw new IllegalArgumentException("uuid and uri are mutually exclusive");
079            parseURI(uri);
080        } else if (uuid == null) {
081            throw new IllegalArgumentException("uuid or uri must be not null");
082        }
083        this.uuid = uuid;
084        this.uri = uri;
085        this.bigEndian = bigEndian;
086    }
087
088    public BulkData(String uri, long offset, int length, boolean bigEndian) {
089        this.uuid = null;
090        this.uriPathEnd = uri.length();
091        this.uri = uri + "?offset=" + offset + "&length=" + length;
092        this.offset = offset;
093        this.length = length;
094        this.bigEndian = bigEndian;
095    }
096
097    public BulkData(String uri, long[] offsets, int[] lengths, boolean bigEndian) {
098        if (offsets.length == 0)
099            throw new IllegalArgumentException("offsets.length == 0");
100
101        if (offsets.length != lengths.length)
102            throw new IllegalArgumentException(
103                    "offsets.length[" + offsets.length
104                    + "] != lengths.length[" + lengths.length + "]");
105
106        this.uuid = null;
107        this.uriPathEnd = uri.length();
108        this.uri = appendQuery(uri, offsets, lengths);
109        this.offsets = offsets.clone();
110        this.lengths = lengths.clone();
111        this.bigEndian = bigEndian;
112    }
113
114    /**
115     * Returns a {@code BulkData} instance combining all {@code BulkData} instances in {@code bulkDataFragments}.
116     *
117     * @param  bulkDataFragments {@code Fragments} instance with {@code BulkData} instances
118     *         referencing individual fragments
119     * @return a {@code BulkData} instance combining all {@code BulkData} instances in {@code bulkDataFragments}.
120     * @throws ClassCastException if {@code bulkDataFragments} contains {@code byte[]}
121     * @throws IllegalArgumentException if {@code bulkDataFragments} contains URIs referencing different Resources
122     *         or without Query Parameter {@code length}.
123     */
124    public static BulkData fromFragments(Fragments bulkDataFragments) {
125        int size = bulkDataFragments.size();
126        String uri = null;
127        long[] offsets = new long[size];
128        int[] lengths = new int[size];
129        for (int i = 0; i < size; i++) {
130            Object value = bulkDataFragments.get(i);
131            if (value == Value.NULL)
132                continue;
133
134            BulkData bulkdata = (BulkData) value;
135            String uriWithoutQuery = bulkdata.uriWithoutQuery();
136            if (uri == null)
137                uri = uriWithoutQuery;
138            else if (!uri.equals(uriWithoutQuery))
139                throw new IllegalArgumentException("BulkData URIs references different Resources");
140            if (bulkdata.length() == -1)
141                throw new IllegalArgumentException("BulkData Reference with unspecified length");
142            offsets[i] = bulkdata.offset();
143            lengths[i] = bulkdata.length();
144        }
145        return new BulkData(uri, offsets, lengths, false);
146    }
147
148    /**
149     * Returns {@code true}, if the URI of this {@code BulkData} instance specifies offset and length of individual
150     * data fragments by Query Parameters {@code offsets} and {@code lengths} and therefore can be converted
151     * by {@link #toFragments} to a {@code Fragments} instance containing {@code BulkData} instances
152     * referencing individual fragments.
153     *
154     * @return {@code true} if this {@code BulkData} instance can be converted to a {@code Fragments} instance
155     * by {@link #toFragments}
156     */
157    public boolean hasFragments() {
158        return  offsets != null && lengths != null;
159    }
160
161    /**
162     * Returns a {@code Fragments} instance with containing {@code BulkData} instances referencing
163     * individual fragments, referenced by this {@code BulkData} instances.
164     *
165     * @param  privateCreator
166     * @param  tag
167     * @param  vr
168     * @return {@code Fragments} instance with containing {@code BulkData} instances referencing
169     * individual fragments, referenced by this {@code BulkData} instances
170     * @throws UnsupportedOperationException, if the URI {@code BulkData} instance does not specify
171     * offset and length of individual data fragments by Query Parameters {@code offsets} and {@code lengths}
172     */
173    public Fragments toFragments (String privateCreator, int tag, VR vr) {
174        if (offsets == null || lengths == null)
175            throw new UnsupportedOperationException();
176
177        if (offsets.length != lengths.length)
178            throw new IllegalStateException("offsets.length[" + offsets.length
179                    + "] != lengths.length[" + lengths.length + "]");
180
181        Fragments fragments = new Fragments(privateCreator, tag, vr, bigEndian, lengths.length);
182        String uriWithoutQuery = uriWithoutQuery();
183        for (int i = 0; i < lengths.length; i++)
184            fragments.add(lengths[i] == 0
185                    ? Value.NULL
186                    : new BulkData(uriWithoutQuery, offsets[i], lengths[i], bigEndian));
187        return fragments;
188    }
189
190    private void parseURI(String uri) {
191        int index = uri.indexOf('?');
192        if (index == -1) {
193            uriPathEnd = uri.length();
194            return;
195        }
196        uriPathEnd = index;
197        if (uri.startsWith("offset=", index+1))
198            parseURIWithOffset(uri, index+8);
199        else if (uri.startsWith("offsets=", index+1))
200            parseURIWithOffsets(uri, index+9);
201    }
202
203    private void parseURIWithOffset(String uri, int from) {
204        int index = uri.indexOf("&length=");
205        if (index == -1)
206            return;
207
208        try {
209            offset = Long.parseLong(uri.substring(from, index));
210            length = Integer.parseInt(uri.substring(index + 8));
211        } catch (NumberFormatException e) {}
212    }
213
214    private void parseURIWithOffsets(String uri, int from) {
215        int index = uri.indexOf("&lengths=");
216        if (index == -1)
217            return;
218        try {
219            offsets = parseLongs(uri.substring(from, index));
220            lengths = parseInts(uri.substring(index + 9));
221        } catch (NumberFormatException e) {}
222    }
223
224    private static long[] parseLongs(String s) {
225        String[] ss = StringUtils.split(s, ',');
226        long[] longs = new long[ss.length];
227        for (int i = 0; i < ss.length; i++) {
228            longs[i] = Long.parseLong(ss[i]);
229        }
230        return longs;
231    }
232
233    private static int[] parseInts(String s) {
234        String[] ss = StringUtils.split(s, ',');
235        int[] ints = new int[ss.length];
236        for (int i = 0; i < ss.length; i++) {
237            ints[i] = Integer.parseInt(ss[i]);
238        }
239        return ints;
240    }
241
242    private String appendQuery(String uri, long[] offsets, int[] lengths) {
243        StringBuilder sb = new StringBuilder(uri);
244        sb.append( "?offsets=");
245        for (long offset : offsets)
246            sb.append(offset).append(',');
247        sb.setLength(sb.length()-1);
248        sb.append("&lengths=");
249        for (int length : lengths)
250            sb.append(length).append(',');
251        sb.setLength(sb.length() - 1);
252        return sb.toString();
253    }
254
255    public long offset() {
256        return offset;
257    }
258
259    public int length() {
260        return length;
261    }
262
263    public long[] offsets() {
264        return offsets;
265    }
266
267    public int[] lengths() {
268        return lengths;
269    }
270
271    @Override
272    public boolean isEmpty() {
273        return length == 0;
274    }
275
276    @Override
277    public String toString() {
278        return "BulkData[uuid=" + uuid + ", uri=" +  uri + ", bigEndian=" + bigEndian  + "]";
279    }
280
281    public String getURIOrUUID() {
282        return (uri != null) ? uri : uuid;
283    }
284
285    public File getFile() {
286        try {
287            return new File(new URI(uriWithoutQuery()));
288        } catch (URISyntaxException e) {
289            throw new IllegalStateException("uri: " + uri);
290        } catch (IllegalArgumentException e) {
291            throw new IllegalStateException("uri: " + uri);
292        }
293    }
294
295    public String uriWithoutQuery() {
296        if (uri == null)
297            throw new IllegalStateException("uri: null");
298
299        return uri.substring(0, uriPathEnd);
300    }
301
302    public InputStream openStream() throws IOException {
303        if (uri == null)
304            throw new IllegalStateException("uri: null");
305 
306        if (!uri.startsWith("file:"))
307            return new URL(uri).openStream();
308
309        InputStream in = new FileInputStream(getFile());
310        StreamUtils.skipFully(in, offset);
311        return in;
312
313    }
314
315    @Override
316    public int calcLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) {
317        if (length == -1)
318            throw new UnsupportedOperationException();
319 
320        return (length + 1) & ~1;
321    }
322
323    @Override
324    public int getEncodedLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) {
325        return (length == -1) ? -1 : ((length + 1) & ~1);
326    }
327
328    @Override
329    public byte[] toBytes(VR vr, boolean bigEndian) throws IOException {
330        if (length == -1)
331            throw new UnsupportedOperationException();
332
333        if (length == 0)
334            return ByteUtils.EMPTY_BYTES;
335
336        InputStream in = openStream();
337        try {
338            byte[] b = new byte[length];
339            StreamUtils.readFully(in, b, 0, b.length);
340            if (this.bigEndian != bigEndian) {
341                vr.toggleEndian(b, false);
342            }
343            return b;
344        } finally {
345            in.close();
346        }
347
348    }
349
350    @Override
351    public void writeTo(DicomOutputStream out, VR vr) throws IOException {
352        InputStream in = openStream();
353        try {
354            if (this.bigEndian != out.isBigEndian())
355                StreamUtils.copy(in, out, length, vr.numEndianBytes());
356            else
357                StreamUtils.copy(in, out, length);
358            if ((length & 1) != 0)
359                out.write(vr.paddingByte());
360        } finally {
361            in.close();
362        }
363    }
364
365    public void serializeTo(ObjectOutputStream oos) throws IOException {
366        oos.writeUTF(StringUtils.maskNull(uuid, ""));
367        oos.writeUTF(StringUtils.maskNull(uri, ""));
368        oos.writeBoolean(bigEndian);
369    }
370
371    public static Value deserializeFrom(ObjectInputStream ois)
372            throws IOException {
373        return new BulkData(
374            StringUtils.maskEmpty(ois.readUTF(), null),
375            StringUtils.maskEmpty(ois.readUTF(), null),
376            ois.readBoolean());
377    }
378
379    @Override
380    public boolean equals(Object obj) {
381        if (this == obj)
382            return true;
383        if (obj == null)
384            return false;
385        if (getClass() != obj.getClass())
386            return false;
387        BulkData other = (BulkData) obj;
388        if (bigEndian != other.bigEndian)
389            return false;
390        if (uri == null) {
391            if (other.uri != null)
392                return false;
393        } else if (!uri.equals(other.uri))
394            return false;
395        if (uuid == null) {
396            if (other.uuid != null)
397                return false;
398        } else if (!uuid.equals(other.uuid))
399            return false;
400        return true;
401    }
402
403    @Override
404    public int hashCode() {
405        final int prime = 31;
406        int result = 1;
407        result = prime * result + (bigEndian ? 1231 : 1237);
408        result = prime * result + ((uri == null) ? 0 : uri.hashCode());
409        result = prime * result + ((uuid == null) ? 0 : uuid.hashCode());
410        return result;
411    }
412
413}