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.mkkos;
040
041import java.io.BufferedOutputStream;
042import java.io.File;
043import java.io.FileDescriptor;
044import java.io.FileOutputStream;
045import java.io.IOException;
046import java.text.MessageFormat;
047import java.util.Date;
048import java.util.Properties;
049import java.util.ResourceBundle;
050
051import org.apache.commons.cli.CommandLine;
052import org.apache.commons.cli.MissingOptionException;
053import org.apache.commons.cli.OptionBuilder;
054import org.apache.commons.cli.OptionGroup;
055import org.apache.commons.cli.Options;
056import org.apache.commons.cli.ParseException;
057import org.dcm4che3.data.Tag;
058import org.dcm4che3.data.UID;
059import org.dcm4che3.data.Attributes;
060import org.dcm4che3.data.Sequence;
061import org.dcm4che3.data.VR;
062import org.dcm4che3.io.DicomEncodingOptions;
063import org.dcm4che3.io.DicomOutputStream;
064import org.dcm4che3.tool.common.CLIUtils;
065import org.dcm4che3.tool.common.DicomFiles;
066import org.dcm4che3.util.UIDUtils;
067
068/**
069 * @author Gunter Zeilinger <gunterze@gmail.com>
070 * @author Michael Backhaus <michael.backhaus@agfa.com>
071 */
072public class MkKOS {
073
074    private static ResourceBundle rb =
075        ResourceBundle.getBundle("org.dcm4che3.tool.mkkos.messages");
076
077    private static final int[] PATIENT_AND_STUDY_ATTRS = {
078        Tag.SpecificCharacterSet,
079        Tag.StudyDate,
080        Tag.StudyTime,
081        Tag.AccessionNumber,
082        Tag.IssuerOfAccessionNumberSequence,
083        Tag.ReferringPhysicianName,
084        Tag.PatientName,
085        Tag.PatientID,
086        Tag.IssuerOfPatientID,
087        Tag.PatientBirthDate,
088        Tag.PatientSex,
089        Tag.StudyInstanceUID,
090        Tag.StudyID 
091    };
092
093    private final Attributes attrs = new Attributes();
094    private String uidSuffix;
095    private String fname;
096    private boolean nofmi;
097    private DicomEncodingOptions encOpts;
098    private String tsuid;
099    private String seriesNumber;
100    private String instanceNumber;
101    private String keyObjectDescription;
102    private String retrieveAET;
103    private String retrieveURL;
104    private String locationUID;
105    private Attributes documentTitle;
106    private Attributes documentTitleModifier;
107    private Properties codes;
108
109    private Attributes kos;
110    private Sequence evidenceSeq;
111    private Sequence contentSeq;
112
113    public String getFname() {
114        return fname;
115    }
116        
117    public final void setUIDSuffix(String uidSuffix) {
118        this.uidSuffix = uidSuffix;
119    }
120
121    public void setOutputFile(String fname) {
122        this.fname = fname;
123    }
124
125    public void setNoFileMetaInformation(boolean nofmi) {
126        this.nofmi = nofmi;
127    }
128
129    public final void setEncodingOptions(DicomEncodingOptions encOpts) {
130        this.encOpts = encOpts;
131    }
132
133    public final void setTransferSyntax(String tsuid) {
134        this.tsuid = tsuid;
135    }
136
137    public final void setSeriesNumber(String seriesNumber) {
138        this.seriesNumber = seriesNumber;
139    }
140
141    public final void setInstanceNumber(String instanceNumber) {
142        this.instanceNumber = instanceNumber;
143    }
144
145    public final void setKeyObjectDescription(String keyObjectDescription) {
146        this.keyObjectDescription = keyObjectDescription;
147    }
148
149    public void setRetrieveAET(String retrieveAET) {
150        this.retrieveAET = retrieveAET;
151    }
152
153    public void setRetrieveURL(String retrieveURL) {
154        this.retrieveURL = retrieveURL;
155    }
156
157    public void setLocationUID(String locationUID) {
158        this.locationUID = locationUID;
159    }
160
161    public final void setCodes(Properties codes) {
162        this.codes = codes;
163    }
164
165    public final void setDocumentTitle(Attributes codeItem) {
166        this.documentTitle = codeItem;
167    }
168
169    public final void setDocumentTitleModifier(Attributes codeItem) {
170        this.documentTitleModifier = codeItem;
171    }
172
173    @SuppressWarnings("unchecked")
174    public static void main(String[] args) throws Exception {
175        try {
176            CommandLine cl = parseComandLine(args);
177            final MkKOS main = new MkKOS();
178            configure(main, cl);
179            System.out.println(rb.getString("scanning"));
180            DicomFiles.scan(cl.getArgList(), new DicomFiles.Callback() {
181                
182                @Override
183                public boolean dicomFile(File f, Attributes fmi,
184                        long dsPos, Attributes ds) {
185                    return main.addInstance(ds);
186                }
187            });
188            System.out.println();
189            main.writeKOS();
190            System.out.println(
191                    MessageFormat.format(rb.getString("stored"), main.fname));
192        } catch (ParseException e) {
193            System.err.println("mkkos: " + e.getMessage());
194            System.err.println(rb.getString("try"));
195            System.exit(2);
196        }
197    }
198
199    private static CommandLine parseComandLine(String[] args)
200            throws ParseException{
201        Options opts = new Options();
202        CLIUtils.addCommonOptions(opts);
203        addOptions(opts);
204        CommandLine cl = CLIUtils.parseComandLine(args, opts, rb, MkKOS.class);
205        if (cl.getArgList().isEmpty())
206            throw new ParseException(rb.getString("missing"));
207        return cl;
208    }
209
210    @SuppressWarnings("static-access")
211    public static void addOptions(Options opts) {
212        opts.addOption(OptionBuilder
213                .hasArg()
214                .withArgName("code")
215                .withDescription(rb.getString("title"))
216                .withLongOpt("title")
217                .create());
218        opts.addOption(OptionBuilder
219                .hasArg()
220                .withArgName("code")
221                .withDescription(rb.getString("modifier"))
222                .withLongOpt("modifier")
223                .create());
224        opts.addOption(OptionBuilder
225                .hasArg()
226                .withArgName("file|url")
227                .withDescription(rb.getString("code-config"))
228                .withLongOpt("code-config")
229                .create());
230        opts.addOption(OptionBuilder
231                .hasArg()
232                .withArgName("text")
233                .withDescription(rb.getString("desc"))
234                .withLongOpt("desc")
235                .create());
236        opts.addOption(OptionBuilder
237                .hasArg()
238                .withArgName("aet")
239                .withDescription(rb.getString("retrieve-aet"))
240                .withLongOpt("retrieve-aet")
241                .create());
242        opts.addOption(OptionBuilder
243                .hasArg()
244                .withArgName("url")
245                .withDescription(rb.getString("retrieve-url"))
246                .withLongOpt("retrieve-url")
247                .create());
248        opts.addOption(OptionBuilder
249                .hasArg()
250                .withArgName("uid")
251                .withDescription(rb.getString("location-uid"))
252                .withLongOpt("location-uid")
253                .create());
254        opts.addOption(OptionBuilder
255                .hasArg()
256                .withArgName("no")
257                .withDescription(rb.getString("series-no"))
258                .withLongOpt("series-no")
259                .create());
260        opts.addOption(OptionBuilder
261                .hasArg()
262                .withArgName("no")
263                .withDescription(rb.getString("inst-no"))
264                .withLongOpt("inst-no")
265                .create());
266       opts.addOption(OptionBuilder
267               .hasArg()
268               .withArgName("file")
269               .withDescription(rb.getString("o-file"))
270               .create("o"));
271       OptionGroup group = new OptionGroup();
272       group.addOption(OptionBuilder
273               .withLongOpt("no-fmi")
274               .withDescription(rb.getString("no-fmi"))
275               .create("F"));
276       group.addOption(OptionBuilder
277               .withLongOpt("transfer-syntax")
278               .hasArg()
279               .withArgName("uid")
280               .withDescription(rb.getString("transfer-syntax"))
281               .create("t"));
282       opts.addOptionGroup(group);
283       opts.addOption(OptionBuilder
284               .hasArgs()
285               .withArgName("[seq/]attr=value")
286               .withValueSeparator('=')
287               .withDescription(rb.getString("set"))
288               .create("s"));
289       opts.addOption(OptionBuilder
290               .hasArg()
291               .withArgName("suffix")
292               .withDescription(rb.getString("uid-suffix"))
293               .withLongOpt("uid-suffix")
294               .create(null));
295       CLIUtils.addEncodingOptions(opts);
296   }
297
298    private static void configure(MkKOS main, CommandLine cl) throws Exception {
299        main.setCodes(CLIUtils.loadProperties(
300                cl.getOptionValue("code-config", "resource:code.properties"),
301                null));
302        main.setDocumentTitle(main.toCodeItem(documentTitleOf(cl)));
303        if (cl.hasOption("modifier"))
304            main.setDocumentTitleModifier(
305                    main.toCodeItem(cl.getOptionValue("modifier")));
306        main.setKeyObjectDescription(cl.getOptionValue("desc"));
307        main.setRetrieveAET(cl.getOptionValue("retrieve-aet", null));
308        main.setRetrieveURL(cl.getOptionValue("retrieve-url", null));
309        main.setLocationUID(cl.getOptionValue("location-uid", null));
310        main.setSeriesNumber(cl.getOptionValue("series-no", "999"));
311        main.setInstanceNumber(cl.getOptionValue("inst-no", "1"));
312        main.setOutputFile(outputFileOf(cl));
313        main.setNoFileMetaInformation(cl.hasOption("F"));
314        main.setTransferSyntax(cl.getOptionValue("t", UID.ExplicitVRLittleEndian));
315        main.setEncodingOptions(CLIUtils.encodingOptionsOf(cl));
316        CLIUtils.addAttributes(main.attrs, cl.getOptionValues("s"));
317        main.setUIDSuffix(cl.getOptionValue("uid-suffix"));
318    }
319
320    public static String outputFileOf(CommandLine cl) throws MissingOptionException {
321        if (!cl.hasOption("o"))
322            throw new MissingOptionException(rb.getString("missing-o-file"));
323        return cl.getOptionValue("o");
324    }
325
326    private static String documentTitleOf(CommandLine cl) throws MissingOptionException {
327        if (!cl.hasOption("title"))
328            throw new MissingOptionException(rb.getString("missing-title"));
329        return cl.getOptionValue("title");
330    }
331
332    public Attributes toCodeItem(String codeValue) {
333        if (codes == null)
334            throw new IllegalStateException("codes not initialized");
335        String codeMeaning = codes.getProperty(codeValue);
336        if (codeMeaning == null)
337            throw new IllegalArgumentException("undefined code value: "
338                        + codeValue);
339        int endDesignator = codeValue.indexOf('-');
340        Attributes attrs = new Attributes(3);
341        attrs.setString(Tag.CodeValue, VR.SH,
342                endDesignator >= 0
343                    ? codeValue.substring(endDesignator + 1)
344                    : codeValue);
345        attrs.setString(Tag.CodingSchemeDesignator, VR.SH,
346                endDesignator >= 0
347                    ? codeValue.substring(0, endDesignator)
348                    : "DCM");
349        attrs.setString(Tag.CodeMeaning, VR.LO, codeMeaning);
350        return attrs;
351    }
352
353    public boolean addInstance(Attributes inst) {
354        CLIUtils.updateAttributes(inst, attrs, uidSuffix);
355        String studyIUID = inst.getString(Tag.StudyInstanceUID);
356        String seriesIUID = inst.getString(Tag.SeriesInstanceUID);
357        String iuid = inst.getString(Tag.SOPInstanceUID);
358        String cuid = inst.getString(Tag.SOPClassUID);
359        if (studyIUID == null || seriesIUID == null || iuid == null || cuid == null)
360            return false;
361        if (kos == null)
362            kos = createKOS(inst);
363        refSOPSeq(refSeriesSeq(studyIUID), seriesIUID).add(refSOP(cuid, iuid));
364        contentSeq.add(contentItem(valueTypeOf(inst), refSOP(cuid, iuid)));
365        return true;
366    }
367
368    public void writeKOS() throws IOException {
369        DicomOutputStream dos = new DicomOutputStream(
370                new BufferedOutputStream(fname != null 
371                        ? new FileOutputStream(fname)
372                        : new FileOutputStream(FileDescriptor.out)),
373                nofmi ? UID.ImplicitVRLittleEndian
374                      : UID.ExplicitVRLittleEndian);
375        dos.setEncodingOptions(encOpts);
376        try {
377            dos.writeDataset(
378                    nofmi ? null : kos.createFileMetaInformation(tsuid),
379                    kos);
380        } finally {
381            dos.close();
382        }
383    }
384
385    private Sequence refSeriesSeq(String studyIUID) {
386        for (Attributes refStudy : evidenceSeq)
387            if (studyIUID.equals(refStudy.getString(Tag.StudyInstanceUID)))
388                return refStudy.getSequence(Tag.ReferencedSeriesSequence);
389
390        Attributes refStudy = new Attributes(2);
391        Sequence refSeriesSeq = refStudy.newSequence(Tag.ReferencedSeriesSequence, 10);
392        refStudy.setString(Tag.StudyInstanceUID, VR.UI, studyIUID);
393        evidenceSeq.add(refStudy);
394        return refSeriesSeq;
395    }
396
397    private Sequence refSOPSeq(Sequence refSeriesSeq , String seriesIUID) {
398        for (Attributes refSeries : refSeriesSeq)
399            if (seriesIUID.equals(refSeries.getString(Tag.SeriesInstanceUID)))
400                return refSeries.getSequence(Tag.ReferencedSOPSequence);
401
402        Attributes refSeries = new Attributes(5);
403        if (retrieveAET != null)
404            refSeries.setString(Tag.RetrieveAETitle, VR.AE, retrieveAET);
405        if (retrieveURL != null)
406            refSeries.setString(Tag.RetrieveURL, VR.UR, retrieveURL);
407        Sequence refSOPSeq = refSeries.newSequence(Tag.ReferencedSOPSequence, 100);
408        refSeries.setString(Tag.SeriesInstanceUID, VR.UI, seriesIUID);
409        if (locationUID != null)
410            refSeries.setString(Tag.RetrieveLocationUID, VR.UI, locationUID);
411        refSeriesSeq.add(refSeries);
412        return refSOPSeq;
413    }
414
415    private String valueTypeOf(Attributes inst) {
416        return inst.contains(Tag.PhotometricInterpretation) ? "IMAGE"
417                      : inst.contains(Tag.WaveformSequence) ? "WAVEFORM"
418                                                            : "COMPOSITE";
419    }
420
421    private Attributes refSOP(String cuid, String iuid) {
422        Attributes item = new Attributes(2);
423        item.setString(Tag.ReferencedSOPClassUID, VR.UI, cuid);
424        item.setString(Tag.ReferencedSOPInstanceUID, VR.UI, iuid);
425        return item;
426    }
427
428    private Attributes createKOS(Attributes inst) {
429        Attributes attrs = new Attributes(inst, PATIENT_AND_STUDY_ATTRS);
430        attrs.setString(Tag.SOPClassUID, VR.UI, UID.KeyObjectSelectionDocumentStorage);
431        attrs.setString(Tag.SOPInstanceUID, VR.UI, UIDUtils.createUID());
432        attrs.setDate(Tag.ContentDateAndTime, new Date());
433        attrs.setString(Tag.Modality, VR.CS, "KO");
434        attrs.setNull(Tag.ReferencedPerformedProcedureStepSequence, VR.SQ);
435        attrs.setString(Tag.SeriesInstanceUID, VR.UI, UIDUtils.createUID());
436        attrs.setString(Tag.SeriesNumber, VR.IS, seriesNumber);
437        attrs.setString(Tag.InstanceNumber, VR.IS, instanceNumber);
438        attrs.setString(Tag.ValueType, VR.CS, "CONTAINER");
439        attrs.setString(Tag.ContinuityOfContent, VR.CS, "SEPARATE");
440        attrs.newSequence(Tag.ConceptNameCodeSequence, 1).add(documentTitle);
441        evidenceSeq = attrs.newSequence(Tag.CurrentRequestedProcedureEvidenceSequence, 1);
442        attrs.newSequence(Tag.ContentTemplateSequence, 1).add(templateIdentifier());
443        contentSeq = attrs.newSequence(Tag.ContentSequence, 1);
444        if (documentTitleModifier != null)
445            contentSeq.add(documentTitleModifier());
446        if (keyObjectDescription != null)
447            contentSeq.add(keyObjectDescription());
448        return attrs;
449    }
450
451    private Attributes templateIdentifier() {
452        Attributes attrs = new Attributes(2);
453        attrs.setString(Tag.MappingResource, VR.CS, "DCMR");
454        attrs.setString(Tag.TemplateIdentifier, VR.CS, "2010");
455        return attrs ;
456    }
457
458    private Attributes documentTitleModifier() {
459        Attributes item = new Attributes(4);
460        item.setString(Tag.RelationshipType, VR.CS, "HAS CONCEPT MOD");
461        item.setString(Tag.ValueType, VR.CS, "CODE");
462        item.newSequence(Tag.ConceptNameCodeSequence, 1).add(toCodeItem("DCM-113011"));
463        item.newSequence(Tag.ConceptCodeSequence, 1).add(documentTitleModifier);
464        return item;
465    }
466
467    private Attributes keyObjectDescription() {
468        Attributes item = new Attributes(4);
469        item.setString(Tag.RelationshipType, VR.CS, "CONTAINS");
470        item.setString(Tag.ValueType, VR.CS, "TEXT");
471        item.newSequence(Tag.ConceptNameCodeSequence, 1).add(toCodeItem("DCM-113012"));
472        item.setString(Tag.TextValue, VR.UT, keyObjectDescription);
473        return item;
474    }
475
476    private Attributes contentItem(String valueType, Attributes refSOP) {
477        Attributes item = new Attributes(3);
478        item.setString(Tag.RelationshipType, VR.CS, "CONTAINS");
479        item.setString(Tag.ValueType, VR.CS, valueType);
480        item.newSequence(Tag.ReferencedSOPSequence, 1).add(refSOP);
481        return item;
482    }
483
484}