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}