/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is part of dcm4che, an implementation of DICOM(TM) in
* Java(TM), hosted at https://github.com/gunterze/dcm4che.
*
* The Initial Developer of the Original Code is
* Agfa Healthcare.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* See @authors listed below
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.dcm4che3.data;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import org.dcm4che3.io.DicomEncodingOptions;
import org.dcm4che3.io.DicomOutputStream;
import org.dcm4che3.util.ByteUtils;
import org.dcm4che3.util.StreamUtils;
import org.dcm4che3.util.StringUtils;
/**
* @author Gunter Zeilinger <gunterze@gmail.com>
*/
public class BulkData implements Value {
public static final int MAGIC_LEN = 0xfbfb;
public final String uri;
public final String uuid;
public final boolean bigEndian;
// derived fields, not considered for equals/hashCode:
private int uriPathEnd;
private long offset;
private int length = -1;
private long[] offsets;
private int[] lengths;
public BulkData(String uuid, String uri, boolean bigEndian) {
if (uri != null) {
if (uuid != null)
throw new IllegalArgumentException("uuid and uri are mutually exclusive");
parseURI(uri);
} else if (uuid == null) {
throw new IllegalArgumentException("uuid or uri must be not null");
}
this.uuid = uuid;
this.uri = uri;
this.bigEndian = bigEndian;
}
public BulkData(String uri, long offset, int length, boolean bigEndian) {
this.uuid = null;
this.uriPathEnd = uri.length();
this.uri = uri + "?offset=" + offset + "&length=" + length;
this.offset = offset;
this.length = length;
this.bigEndian = bigEndian;
}
public BulkData(String uri, long[] offsets, int[] lengths, boolean bigEndian) {
if (offsets.length == 0)
throw new IllegalArgumentException("offsets.length == 0");
if (offsets.length != lengths.length)
throw new IllegalArgumentException(
"offsets.length[" + offsets.length
+ "] != lengths.length[" + lengths.length + "]");
this.uuid = null;
this.uriPathEnd = uri.length();
this.uri = appendQuery(uri, offsets, lengths);
this.offsets = offsets.clone();
this.lengths = lengths.clone();
this.bigEndian = bigEndian;
}
/**
* Returns a {@code BulkData} instance combining all {@code BulkData} instances in {@code bulkDataFragments}.
*
* @param bulkDataFragments {@code Fragments} instance with {@code BulkData} instances
* referencing individual fragments
* @return a {@code BulkData} instance combining all {@code BulkData} instances in {@code bulkDataFragments}.
* @throws ClassCastException if {@code bulkDataFragments} contains {@code byte[]}
* @throws IllegalArgumentException if {@code bulkDataFragments} contains URIs referencing different Resources
* or without Query Parameter {@code length}.
*/
public static BulkData fromFragments(Fragments bulkDataFragments) {
int size = bulkDataFragments.size();
String uri = null;
long[] offsets = new long[size];
int[] lengths = new int[size];
for (int i = 0; i < size; i++) {
Object value = bulkDataFragments.get(i);
if (value == Value.NULL)
continue;
BulkData bulkdata = (BulkData) value;
String uriWithoutQuery = bulkdata.uriWithoutQuery();
if (uri == null)
uri = uriWithoutQuery;
else if (!uri.equals(uriWithoutQuery))
throw new IllegalArgumentException("BulkData URIs references different Resources");
if (bulkdata.length() == -1)
throw new IllegalArgumentException("BulkData Reference with unspecified length");
offsets[i] = bulkdata.offset();
lengths[i] = bulkdata.length();
}
return new BulkData(uri, offsets, lengths, false);
}
/**
* Returns {@code true}, if the URI of this {@code BulkData} instance specifies offset and length of individual
* data fragments by Query Parameters {@code offsets} and {@code lengths} and therefore can be converted
* by {@link #toFragments} to a {@code Fragments} instance containing {@code BulkData} instances
* referencing individual fragments.
*
* @return {@code true} if this {@code BulkData} instance can be converted to a {@code Fragments} instance
* by {@link #toFragments}
*/
public boolean hasFragments() {
return offsets != null && lengths != null;
}
/**
* Returns a {@code Fragments} instance with containing {@code BulkData} instances referencing
* individual fragments, referenced by this {@code BulkData} instances.
*
* @param privateCreator
* @param tag
* @param vr
* @return {@code Fragments} instance with containing {@code BulkData} instances referencing
* individual fragments, referenced by this {@code BulkData} instances
* @throws UnsupportedOperationException, if the URI {@code BulkData} instance does not specify
* offset and length of individual data fragments by Query Parameters {@code offsets} and {@code lengths}
*/
public Fragments toFragments (String privateCreator, int tag, VR vr) {
if (offsets == null || lengths == null)
throw new UnsupportedOperationException();
if (offsets.length != lengths.length)
throw new IllegalStateException("offsets.length[" + offsets.length
+ "] != lengths.length[" + lengths.length + "]");
Fragments fragments = new Fragments(privateCreator, tag, vr, bigEndian, lengths.length);
String uriWithoutQuery = uriWithoutQuery();
for (int i = 0; i < lengths.length; i++)
fragments.add(lengths[i] == 0
? Value.NULL
: new BulkData(uriWithoutQuery, offsets[i], lengths[i], bigEndian));
return fragments;
}
private void parseURI(String uri) {
int index = uri.indexOf('?');
if (index == -1) {
uriPathEnd = uri.length();
return;
}
uriPathEnd = index;
if (uri.startsWith("offset=", index+1))
parseURIWithOffset(uri, index+8);
else if (uri.startsWith("offsets=", index+1))
parseURIWithOffsets(uri, index+9);
}
private void parseURIWithOffset(String uri, int from) {
int index = uri.indexOf("&length=");
if (index == -1)
return;
try {
offset = Long.parseLong(uri.substring(from, index));
length = Integer.parseInt(uri.substring(index + 8));
} catch (NumberFormatException e) {}
}
private void parseURIWithOffsets(String uri, int from) {
int index = uri.indexOf("&lengths=");
if (index == -1)
return;
try {
offsets = parseLongs(uri.substring(from, index));
lengths = parseInts(uri.substring(index + 9));
} catch (NumberFormatException e) {}
}
private static long[] parseLongs(String s) {
String[] ss = StringUtils.split(s, ',');
long[] longs = new long[ss.length];
for (int i = 0; i < ss.length; i++) {
longs[i] = Long.parseLong(ss[i]);
}
return longs;
}
private static int[] parseInts(String s) {
String[] ss = StringUtils.split(s, ',');
int[] ints = new int[ss.length];
for (int i = 0; i < ss.length; i++) {
ints[i] = Integer.parseInt(ss[i]);
}
return ints;
}
private String appendQuery(String uri, long[] offsets, int[] lengths) {
StringBuilder sb = new StringBuilder(uri);
sb.append( "?offsets=");
for (long offset : offsets)
sb.append(offset).append(',');
sb.setLength(sb.length()-1);
sb.append("&lengths=");
for (int length : lengths)
sb.append(length).append(',');
sb.setLength(sb.length() - 1);
return sb.toString();
}
public long offset() {
return offset;
}
public int length() {
return length;
}
public long[] offsets() {
return offsets;
}
public int[] lengths() {
return lengths;
}
@Override
public boolean isEmpty() {
return length == 0;
}
@Override
public String toString() {
return "BulkData[uuid=" + uuid + ", uri=" + uri + ", bigEndian=" + bigEndian + "]";
}
public String getURIOrUUID() {
return (uri != null) ? uri : uuid;
}
public File getFile() {
try {
return new File(new URI(uriWithoutQuery()));
} catch (URISyntaxException e) {
throw new IllegalStateException("uri: " + uri);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("uri: " + uri);
}
}
public String uriWithoutQuery() {
if (uri == null)
throw new IllegalStateException("uri: null");
return uri.substring(0, uriPathEnd);
}
public InputStream openStream() throws IOException {
if (uri == null)
throw new IllegalStateException("uri: null");
if (!uri.startsWith("file:"))
return new URL(uri).openStream();
InputStream in = new FileInputStream(getFile());
StreamUtils.skipFully(in, offset);
return in;
}
@Override
public int calcLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) {
if (length == -1)
throw new UnsupportedOperationException();
return (length + 1) & ~1;
}
@Override
public int getEncodedLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) {
return (length == -1) ? -1 : ((length + 1) & ~1);
}
@Override
public byte[] toBytes(VR vr, boolean bigEndian) throws IOException {
if (length == -1)
throw new UnsupportedOperationException();
if (length == 0)
return ByteUtils.EMPTY_BYTES;
InputStream in = openStream();
try {
byte[] b = new byte[length];
StreamUtils.readFully(in, b, 0, b.length);
if (this.bigEndian != bigEndian) {
vr.toggleEndian(b, false);
}
return b;
} finally {
in.close();
}
}
@Override
public void writeTo(DicomOutputStream out, VR vr) throws IOException {
InputStream in = openStream();
try {
if (this.bigEndian != out.isBigEndian())
StreamUtils.copy(in, out, length, vr.numEndianBytes());
else
StreamUtils.copy(in, out, length);
if ((length & 1) != 0)
out.write(vr.paddingByte());
} finally {
in.close();
}
}
public void serializeTo(ObjectOutputStream oos) throws IOException {
oos.writeUTF(StringUtils.maskNull(uuid, ""));
oos.writeUTF(StringUtils.maskNull(uri, ""));
oos.writeBoolean(bigEndian);
}
public static Value deserializeFrom(ObjectInputStream ois)
throws IOException {
return new BulkData(
StringUtils.maskEmpty(ois.readUTF(), null),
StringUtils.maskEmpty(ois.readUTF(), null),
ois.readBoolean());
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BulkData other = (BulkData) obj;
if (bigEndian != other.bigEndian)
return false;
if (uri == null) {
if (other.uri != null)
return false;
} else if (!uri.equals(other.uri))
return false;
if (uuid == null) {
if (other.uuid != null)
return false;
} else if (!uuid.equals(other.uuid))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (bigEndian ? 1231 : 1237);
result = prime * result + ((uri == null) ? 0 : uri.hashCode());
result = prime * result + ((uuid == null) ? 0 : uuid.hashCode());
return result;
}
}