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.tool.dcmdir; 040 041import java.io.File; 042import java.io.IOException; 043import java.text.MessageFormat; 044import java.util.List; 045import java.util.ResourceBundle; 046 047import org.apache.commons.cli.CommandLine; 048import org.apache.commons.cli.OptionBuilder; 049import org.apache.commons.cli.OptionGroup; 050import org.apache.commons.cli.Options; 051import org.apache.commons.cli.ParseException; 052import org.dcm4che3.data.Tag; 053import org.dcm4che3.data.UID; 054import org.dcm4che3.data.Attributes; 055import org.dcm4che3.data.VR; 056import org.dcm4che3.io.DicomEncodingOptions; 057import org.dcm4che3.io.DicomInputStream; 058import org.dcm4che3.io.DicomInputStream.IncludeBulkData; 059import org.dcm4che3.media.DicomDirReader; 060import org.dcm4che3.media.DicomDirWriter; 061import org.dcm4che3.media.RecordFactory; 062import org.dcm4che3.media.RecordType; 063import org.dcm4che3.tool.common.CLIUtils; 064import org.dcm4che3.tool.common.FilesetInfo; 065import org.dcm4che3.util.SafeClose; 066import org.dcm4che3.util.UIDUtils; 067 068/** 069 * @author Gunter Zeilinger <gunterze@gmail.com> 070 */ 071public class DcmDir { 072 073 private static ResourceBundle rb = 074 ResourceBundle.getBundle("org.dcm4che3.tool.dcmdir.messages"); 075 076 /** default number of characters per line */ 077 private static final int DEFAULT_WIDTH = 78; 078 079 private boolean inUse; 080 private int width = DEFAULT_WIDTH; 081 private DicomEncodingOptions encOpts = DicomEncodingOptions.DEFAULT; 082 private final FilesetInfo fsInfo = new FilesetInfo(); 083 private boolean origSeqLength; 084 private boolean checkDuplicate; 085 086 private File file; 087 private DicomDirReader in; 088 private DicomDirWriter out; 089 private RecordFactory recFact; 090 091 @SuppressWarnings("static-access") 092 private static CommandLine parseComandLine(String[] args) 093 throws ParseException{ 094 Options opts = new Options(); 095 CLIUtils.addCommonOptions(opts); 096 CLIUtils.addFilesetInfoOptions(opts); 097 OptionGroup cmdGroup = new OptionGroup(); 098 addCommandOptions(cmdGroup); 099 opts.addOptionGroup(cmdGroup); 100 opts.addOption(OptionBuilder 101 .withLongOpt("width") 102 .hasArg() 103 .withArgName("col") 104 .withDescription(rb.getString("width")) 105 .create("w")); 106 opts.addOption(null, "in-use", false, rb.getString("in-use")); 107 opts.addOption(null, "orig-seq-len", false, 108 rb.getString("orig-seq-len")); 109 CLIUtils.addEncodingOptions(opts); 110 CommandLine cl = CLIUtils.parseComandLine(args, opts, rb, DcmDir.class); 111 if (cmdGroup.getSelected() == null) 112 throw new ParseException(rb.getString("missing")); 113 return cl; 114 } 115 116 @SuppressWarnings("static-access") 117 private static void addCommandOptions(OptionGroup cmdGroup) { 118 cmdGroup.addOption(OptionBuilder 119 .hasArg() 120 .withArgName("dicomdir") 121 .withDescription(rb.getString("list")) 122 .create("l")); 123 cmdGroup.addOption(OptionBuilder 124 .hasArg() 125 .withArgName("dicomdir") 126 .withDescription(rb.getString("create")) 127 .create("c")); 128 cmdGroup.addOption(OptionBuilder 129 .hasArg() 130 .withArgName("dicomdir") 131 .withDescription(rb.getString("update")) 132 .create("u")); 133 cmdGroup.addOption(OptionBuilder 134 .hasArg() 135 .withArgName("dicomdir") 136 .withDescription(rb.getString("delete")) 137 .create("d")); 138 cmdGroup.addOption(OptionBuilder 139 .hasArg() 140 .withArgName("dicomdir") 141 .withDescription(rb.getString("purge")) 142 .create("p")); 143 cmdGroup.addOption(OptionBuilder 144 .hasArg() 145 .withArgName("dicomdir") 146 .withDescription(rb.getString("compact")) 147 .create("z")); 148 } 149 150 @SuppressWarnings("unchecked") 151 public static void main(String[] args) { 152 try { 153 CommandLine cl = parseComandLine(args); 154 DcmDir main = new DcmDir(); 155 main.setInUse(cl.hasOption("in-use")); 156 main.setEncodingOptions(CLIUtils.encodingOptionsOf(cl)); 157 CLIUtils.configure(main.fsInfo, cl); 158 main.setOriginalSequenceLength(cl.hasOption("orig-seq-len")); 159 if (cl.hasOption("w")) { 160 String s = cl.getOptionValue("w"); 161 try { 162 main.setWidth(Integer.parseInt(s)); 163 } catch (IllegalArgumentException e) { 164 throw new ParseException(MessageFormat.format( 165 rb.getString("illegal-width"), s)); 166 } 167 } 168 try { 169 List<String> argList = cl.getArgList(); 170 long start = System.currentTimeMillis(); 171 if (cl.hasOption("l")) { 172 main.openForReadOnly(new File(cl.getOptionValue("l"))); 173 main.list(); 174 } else if (cl.hasOption("d")) { 175 main.open(new File(cl.getOptionValue("d"))); 176 int num = 0; 177 for (String arg : argList) 178 num += main.removeReferenceTo(new File(arg)); 179 main.close(); 180 long end = System.currentTimeMillis(); 181 System.out.println(); 182 System.out.println(MessageFormat.format( 183 rb.getString("deleted"), 184 num, main.getFile(), (end - start))); 185 } else if (cl.hasOption("p")) { 186 main.open(new File(cl.getOptionValue("p"))); 187 int num = main.purge(); 188 main.close(); 189 long end = System.currentTimeMillis(); 190 System.out.println(MessageFormat.format( 191 rb.getString("purged"), 192 num, main.getFile(), (end - start))); 193 } else if (cl.hasOption("z")) { 194 String fpath = cl.getOptionValue("z"); 195 File f = new File(fpath); 196 File bak = new File(fpath + "~"); 197 main.compact(f, bak); 198 long end = System.currentTimeMillis(); 199 System.out.println(MessageFormat.format( 200 rb.getString("compacted"), 201 f, bak.length(), f.length(), (end - start))); 202 } else { 203 if (cl.hasOption("c")) { 204 main.create(new File(cl.getOptionValue("c"))); 205 } else if (cl.hasOption("u")) { 206 main.open(new File(cl.getOptionValue("u"))); 207 } 208 main.setRecordFactory(new RecordFactory()); 209 int num = 0; 210 for (String arg : argList) 211 num += main.addReferenceTo(new File(arg)); 212 main.close(); 213 long end = System.currentTimeMillis(); 214 System.out.println(); 215 System.out.println(MessageFormat.format( 216 rb.getString("added"), 217 num, main.getFile(), (end - start))); 218 } 219 } finally { 220 main.close(); 221 } 222 } catch (ParseException e) { 223 System.err.println("dcmdir: " + e.getMessage()); 224 System.err.println(rb.getString("try")); 225 System.exit(2); 226 } catch (IOException e) { 227 System.err.println("dcmdir: " + e.getMessage()); 228 e.printStackTrace(); 229 System.exit(2); 230 } 231 } 232 233 public void compact(File f, File bak) throws IOException { 234 File tmp = File.createTempFile("DICOMDIR", null, f.getParentFile()); 235 DicomDirReader r = new DicomDirReader(f); 236 try { 237 fsInfo.setFilesetUID(r.getFileSetUID()); 238 fsInfo.setFilesetID(r.getFileSetID()); 239 fsInfo.setDescriptorFile( 240 r.getDescriptorFile()); 241 fsInfo.setDescriptorFileCharset( 242 r.getDescriptorFileCharacterSet()); 243 create(tmp); 244 copyFrom(r); 245 } finally { 246 close(); 247 try { r.close(); } catch (IOException ignore) {} 248 } 249 bak.delete(); 250 rename(f, bak); 251 rename(tmp, f); 252 } 253 254 private void rename(File from, File to) throws IOException { 255 if (!from.renameTo(to)) 256 throw new IOException( 257 MessageFormat.format(rb.getString("failed-to-rename"), 258 from, to)); 259 } 260 261 public void copyFrom(DicomDirReader r) throws IOException { 262 Attributes rec = r.findFirstRootDirectoryRecordInUse(false); 263 while (rec != null) { 264 copyChildsFrom(r, rec, 265 out.addRootDirectoryRecord(new Attributes(rec))); 266 rec = r.findNextDirectoryRecordInUse(rec, false); 267 } 268 } 269 270 private void copyChildsFrom(DicomDirReader r, Attributes src, 271 Attributes dst) throws IOException { 272 Attributes rec = r.findLowerDirectoryRecordInUse(src, false); 273 while (rec != null) { 274 copyChildsFrom(r, rec, 275 out.addLowerDirectoryRecord(dst, new Attributes(rec))); 276 rec = r.findNextDirectoryRecordInUse(rec, false); 277 } 278 } 279 280 public final File getFile() { 281 return file; 282 } 283 284 public final void setInUse(boolean inUse) { 285 this.inUse = inUse; 286 } 287 288 public final void setOriginalSequenceLength(boolean origSeqLength) { 289 this.origSeqLength = origSeqLength; 290 } 291 292 public final void setEncodingOptions(DicomEncodingOptions encOpts) { 293 this.encOpts = encOpts; 294 } 295 296 public final void setWidth(int width) { 297 if (width < 40) 298 throw new IllegalArgumentException(); 299 this.width = width; 300 } 301 302 public final void setCheckDuplicate(boolean checkDuplicate) { 303 this.checkDuplicate = checkDuplicate; 304 } 305 306 public final void setRecordFactory(RecordFactory recFact) { 307 this.recFact = recFact; 308 } 309 310 public void close() { 311 SafeClose.close(in); 312 in = null; 313 out = null; 314 } 315 316 public void openForReadOnly(File file) throws IOException { 317 this.file = file; 318 in = new DicomDirReader(file); 319 } 320 321 public void create(File file) throws IOException { 322 this.file = file; 323 DicomDirWriter.createEmptyDirectory(file, 324 UIDUtils.createUIDIfNull(fsInfo.getFilesetUID()), 325 fsInfo.getFilesetID(), 326 fsInfo.getDescriptorFile(), 327 fsInfo.getDescriptorFileCharset()); 328 in = out = DicomDirWriter.open(file); 329 out.setEncodingOptions(encOpts); 330 setCheckDuplicate(false); 331 } 332 333 public void open(File file) throws IOException { 334 this.file = file; 335 in = out = DicomDirWriter.open(file); 336 if (!origSeqLength) 337 out.setEncodingOptions(encOpts); 338 setCheckDuplicate(true); 339 } 340 341 public void list() throws IOException { 342 checkIn(); 343 list("File Meta Information:", in.getFileMetaInformation()); 344 list("File-set Information:", in.getFileSetInformation()); 345 list(inUse 346 ? in.findFirstRootDirectoryRecordInUse(false) 347 : in.readFirstRootDirectoryRecord(), 348 new StringBuilder()); 349 } 350 351 private void list(final String header, final Attributes attrs) { 352 System.out.println(header); 353 System.out.println(attrs.toString(Integer.MAX_VALUE, width)); 354 } 355 356 private void list(Attributes rec, StringBuilder index) 357 throws IOException { 358 int indexLen = index.length(); 359 int i = 1; 360 while (rec != null) { 361 index.append(i++).append('.'); 362 list(heading(rec, index), rec); 363 list(inUse 364 ? in.findLowerDirectoryRecordInUse(rec, false) 365 : in.readLowerDirectoryRecord(rec), 366 index); 367 rec = inUse 368 ? in.findNextDirectoryRecordInUse(rec, false) 369 : in.readNextDirectoryRecord(rec); 370 index.setLength(indexLen); 371 }; 372 } 373 374 private String heading(Attributes rec, StringBuilder index) { 375 int prefixLen = index.length(); 376 try { 377 return index.append(' ') 378 .append(rec.getString(Tag.DirectoryRecordType, "")) 379 .append(':').toString(); 380 } finally { 381 index.setLength(prefixLen); 382 } 383 } 384 385 public int addReferenceTo(File f) throws IOException { 386 checkOut(); 387 checkRecordFactory(); 388 int n = 0; 389 if (f.isDirectory()) { 390 for (String s : f.list()) 391 n += addReferenceTo(new File(f, s)); 392 return n; 393 } 394 // do not add reference to DICOMDIR 395 if (f.equals(file)) 396 return 0; 397 398 Attributes fmi; 399 Attributes dataset; 400 DicomInputStream din = null; 401 try { 402 din = new DicomInputStream(f); 403 din.setIncludeBulkData(IncludeBulkData.NO); 404 fmi = din.readFileMetaInformation(); 405 dataset = din.readDataset(-1, Tag.PixelData); 406 } catch (IOException e) { 407 System.out.println(); 408 System.out.println( 409 MessageFormat.format(rb.getString("failed-to-parse"), 410 f, e.getMessage())); 411 return 0; 412 } finally { 413 if (din != null) 414 try { din.close(); } catch (Exception ignore) {} 415 } 416 char prompt = '.'; 417 if (fmi == null) { 418 fmi = dataset.createFileMetaInformation(UID.ImplicitVRLittleEndian); 419 prompt = 'F'; 420 } 421 String iuid = fmi.getString(Tag.MediaStorageSOPInstanceUID, null); 422 if (iuid == null) { 423 System.out.println(); 424 System.out.println(MessageFormat.format( 425 rb.getString("skip-file"), f)); 426 return 0; 427 } 428 String pid = dataset.getString(Tag.PatientID, null); 429 String styuid = dataset.getString(Tag.StudyInstanceUID, null); 430 String seruid = dataset.getString(Tag.SeriesInstanceUID, null); 431 if (styuid != null && seruid != null) { 432 if (pid == null) { 433 dataset.setString(Tag.PatientID, VR.LO, pid = styuid); 434 prompt = prompt == 'F' ? 'P' : 'p'; 435 } 436 Attributes patRec = in.findPatientRecord(pid); 437 if (patRec == null) { 438 patRec = recFact.createRecord(RecordType.PATIENT, null, 439 dataset, null, null); 440 out.addRootDirectoryRecord(patRec); 441 n++; 442 } 443 Attributes studyRec = in.findStudyRecord(patRec, styuid); 444 if (studyRec == null) { 445 studyRec = recFact.createRecord(RecordType.STUDY, null, 446 dataset, null, null); 447 out.addLowerDirectoryRecord(patRec, studyRec); 448 n++; 449 } 450 Attributes seriesRec = in.findSeriesRecord(studyRec, seruid); 451 if (seriesRec == null) { 452 seriesRec = recFact.createRecord(RecordType.SERIES, null, 453 dataset, null, null); 454 out.addLowerDirectoryRecord(studyRec, seriesRec); 455 n++; 456 } 457 Attributes instRec; 458 if (checkDuplicate) { 459 instRec = in.findLowerInstanceRecord(seriesRec, false, iuid); 460 if (instRec != null) { 461 System.out.print('-'); 462 return 0; 463 } 464 } 465 instRec = recFact.createRecord(dataset, fmi, out.toFileIDs(f)); 466 out.addLowerDirectoryRecord(seriesRec, instRec); 467 } else { 468 if (checkDuplicate) { 469 if (in.findRootInstanceRecord(false, iuid) != null) { 470 System.out.print('-'); 471 return 0; 472 } 473 } 474 Attributes instRec = recFact.createRecord(dataset, fmi, 475 out.toFileIDs(f)); 476 out.addRootDirectoryRecord(instRec); 477 prompt = prompt == 'F' ? 'R' : 'r'; 478 } 479 System.out.print(prompt); 480 return n + 1; 481 } 482 483 public int removeReferenceTo(File f) throws IOException { 484 checkOut(); 485 int n = 0; 486 if (f.isDirectory()) { 487 for (String s : f.list()) 488 n += removeReferenceTo(new File(f, s)); 489 return n; 490 } 491 String pid; 492 String styuid; 493 String seruid; 494 String iuid; 495 DicomInputStream din = null; 496 try { 497 din = new DicomInputStream(f); 498 din.setIncludeBulkData(IncludeBulkData.NO); 499 Attributes fmi = din.readFileMetaInformation(); 500 Attributes dataset = din.readDataset(-1, Tag.StudyID); 501 iuid = (fmi != null) 502 ? fmi.getString(Tag.MediaStorageSOPInstanceUID, null) 503 : dataset.getString(Tag.SOPInstanceUID, null); 504 if (iuid == null) { 505 System.out.println(); 506 System.out.println(MessageFormat.format( 507 rb.getString("skip-file"), f)); 508 return 0; 509 } 510 pid = dataset.getString(Tag.PatientID, null); 511 styuid = dataset.getString(Tag.StudyInstanceUID, null); 512 seruid = dataset.getString(Tag.SeriesInstanceUID, null); 513 } catch (IOException e) { 514 System.out.println(); 515 System.out.println( 516 MessageFormat.format(rb.getString("failed-to-parse"), 517 f, e.getMessage())); 518 return 0; 519 } finally { 520 if (din != null) 521 try { din.close(); } catch (Exception ignore) {} 522 } 523 Attributes instRec; 524 if (styuid != null && seruid != null) { 525 Attributes patRec = 526 in.findPatientRecord(pid == null ? styuid : pid); 527 if (patRec == null) { 528 return 0; 529 } 530 Attributes studyRec = in.findStudyRecord(patRec, styuid); 531 if (studyRec == null) { 532 return 0; 533 } 534 Attributes seriesRec = in.findSeriesRecord(studyRec, seruid); 535 if (seriesRec == null) { 536 return 0; 537 } 538 instRec = in.findLowerInstanceRecord(seriesRec, false, iuid); 539 } else { 540 instRec = in.findRootInstanceRecord(false, iuid); 541 } 542 if (instRec == null) { 543 return 0; 544 } 545 out.deleteRecord(instRec); 546 System.out.print('x'); 547 return 1; 548 } 549 550 public void commit() throws IOException { 551 checkOut(); 552 out.commit(); 553 } 554 555 public int purge() throws IOException { 556 checkOut(); 557 return out.purge(); 558 } 559 560 private void checkIn() { 561 if (in == null) 562 throw new IllegalStateException(rb.getString("no-open-file")); 563 } 564 565 private void checkOut() { 566 checkIn(); 567 if (out == null) 568 throw new IllegalStateException(rb.getString("read-only")); 569 } 570 571 private void checkRecordFactory() { 572 if (recFact == null) 573 throw new IllegalStateException(rb.getString("no-record-factory")); 574 } 575 576}