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}