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}