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.media; 040 041import java.io.File; 042import java.io.FileNotFoundException; 043import java.io.IOException; 044import java.util.ArrayList; 045import java.util.Collections; 046import java.util.Comparator; 047import java.util.IdentityHashMap; 048 049import org.dcm4che3.data.Tag; 050import org.dcm4che3.data.UID; 051import org.dcm4che3.data.Attributes; 052import org.dcm4che3.data.VR; 053import org.dcm4che3.io.DicomEncodingOptions; 054import org.dcm4che3.io.DicomOutputStream; 055import org.dcm4che3.io.RAFOutputStreamAdapter; 056import org.dcm4che3.util.ByteUtils; 057import org.dcm4che3.util.StringUtils; 058import org.slf4j.Logger; 059import org.slf4j.LoggerFactory; 060 061/** 062 * @author Gunter Zeilinger <gunterze@gmail.com> 063 */ 064public class DicomDirWriter extends DicomDirReader { 065 066 private final static Logger LOG = 067 LoggerFactory.getLogger(DicomDirWriter.class); 068 069 private final static int KNOWN_INCONSISTENCIES = 0xFFFF; 070 private final static int NO_KNOWN_INCONSISTENCIES = 0; 071 private final static int IN_USE = 0xFFFF; 072 private final static int INACTIVE = 0; 073 074 private final byte[] dirInfoHeader = { 075 0x04, 0x00, 0x00, 0x12, 'U', 'L', 4, 0, 0, 0, 0, 0, 076 0x04, 0x00, 0x02, 0x12, 'U', 'L', 4, 0, 0, 0, 0, 0, 077 0x04, 0x00, 0x12, 0x12, 'U', 'S', 2, 0, 0, 0, 078 0x04, 0x00, 0x20, 0x12, 'S', 'Q', 0, 0, 0, 0, 0, 0 }; 079 080 private final byte[] dirRecordHeader = { 081 0x04, 0x00, 0x00, 0x14, 'U', 'L', 4, 0, 0, 0, 0, 0, 082 0x04, 0x00, 0x10, 0x14, 'U', 'S', 2, 0, 0, 0, 083 0x04, 0x00, 0x20, 0x14, 'U', 'L', 4, 0, 0, 0, 0, 0 }; 084 085 private final DicomOutputStream out; 086 private final int firstRecordPos; 087 private int nextRecordPos; 088 private int rollbackLen = -1; 089 private IdentityHashMap<Attributes,Attributes> lastChildRecords = 090 new IdentityHashMap<Attributes,Attributes>(); 091 private final ArrayList<Attributes> dirtyRecords = 092 new ArrayList<Attributes>(); 093 094 private DicomDirWriter(File file) throws IOException { 095 super(file, "rw"); 096 out = new DicomOutputStream(new RAFOutputStreamAdapter(raf), 097 super.getTransferSyntaxUID()); 098 int seqLen = in.length(); 099 boolean undefSeqLen = seqLen <= 0; 100 setEncodingOptions( 101 new DicomEncodingOptions(false, 102 undefSeqLen, 103 false, 104 undefSeqLen, 105 false)); 106 this.nextRecordPos = this.firstRecordPos = (int) in.getPosition(); 107 if (!isEmpty()) { 108 if (seqLen > 0) 109 this.nextRecordPos += seqLen; 110 else 111 this.nextRecordPos = (int) (raf.length() - 12); 112 } 113 updateDirInfoHeader(); 114 } 115 116 public DicomEncodingOptions getEncodingOptions() { 117 return out.getEncodingOptions(); 118 } 119 120 public void setEncodingOptions(DicomEncodingOptions encOpts) { 121 out.setEncodingOptions(encOpts); 122 } 123 124 public static DicomDirWriter open(File file) throws IOException { 125 if (!file.isFile()) 126 throw new FileNotFoundException(); 127 128 return new DicomDirWriter(file); 129 } 130 131 public static void createEmptyDirectory(File file, String iuid, 132 String id, File descFile, String charset) throws IOException { 133 Attributes fmi = Attributes.createFileMetaInformation(iuid, 134 UID.MediaStorageDirectoryStorage, UID.ExplicitVRLittleEndian); 135 createEmptyDirectory(file, fmi, id, descFile, charset); 136 } 137 138 public static void createEmptyDirectory(File file, Attributes fmi, 139 String id, File descFile, String charset) throws IOException { 140 Attributes fsInfo = 141 createFileSetInformation(file, id, descFile, charset); 142 DicomOutputStream out = new DicomOutputStream(file); 143 try { 144 out.writeDataset(fmi, fsInfo); 145 } finally { 146 out.close(); 147 } 148 } 149 150 private static Attributes createFileSetInformation(File file, String id, 151 File descFile, String charset) { 152 Attributes fsInfo = new Attributes(7); 153 fsInfo.setString(Tag.FileSetID, VR.CS, id); 154 if (descFile != null) { 155 fsInfo.setString(Tag.FileSetDescriptorFileID, VR.CS, 156 toFileIDs(file, descFile)); 157 if (charset != null && !charset.isEmpty()) 158 fsInfo.setString( 159 Tag.SpecificCharacterSetOfFileSetDescriptorFile, 160 VR.CS, charset); 161 } 162 fsInfo.setInt( 163 Tag.OffsetOfTheFirstDirectoryRecordOfTheRootDirectoryEntity, 164 VR.UL, 0); 165 fsInfo.setInt( 166 Tag.OffsetOfTheLastDirectoryRecordOfTheRootDirectoryEntity, 167 VR.UL, 0); 168 fsInfo.setInt(Tag.FileSetConsistencyFlag, VR.US, 0); 169 fsInfo.setNull(Tag.DirectoryRecordSequence, VR.SQ); 170 return fsInfo; 171 } 172 173 public synchronized Attributes addRootDirectoryRecord(Attributes rec) 174 throws IOException { 175 Attributes lastRootRecord = readLastRootDirectoryRecord(); 176 if (lastRootRecord == null) { 177 writeRecord(firstRecordPos, rec); 178 setOffsetOfFirstRootDirectoryRecord(firstRecordPos); 179 } else { 180 addRecord(Tag.OffsetOfTheNextDirectoryRecord, lastRootRecord, rec); 181 } 182 setOffsetOfLastRootDirectoryRecord((int) rec.getItemPosition()); 183 return rec; 184 } 185 186 public synchronized Attributes addLowerDirectoryRecord( 187 Attributes parentRec, Attributes rec) throws IOException { 188 Attributes prevRec = lastChildRecords.get(parentRec); 189 if (prevRec == null) 190 prevRec = findLastLowerDirectoryRecord(parentRec); 191 192 if (prevRec != null) 193 addRecord(Tag.OffsetOfTheNextDirectoryRecord, prevRec, rec); 194 else 195 addRecord(Tag.OffsetOfReferencedLowerLevelDirectoryEntity, 196 parentRec, rec); 197 198 lastChildRecords.put(parentRec, rec); 199 return rec; 200 } 201 202 public synchronized Attributes findOrAddPatientRecord(Attributes rec) throws IOException { 203 Attributes patRec = super.findPatientRecord(rec.getString(Tag.PatientID)); 204 return patRec != null ? patRec : addRootDirectoryRecord(rec); 205 } 206 207 public synchronized Attributes findOrAddStudyRecord(Attributes patRec, Attributes rec) 208 throws IOException { 209 Attributes studyRec = super.findStudyRecord(patRec, rec.getString(Tag.StudyInstanceUID)); 210 return studyRec != null ? studyRec : addLowerDirectoryRecord(patRec, rec); 211 } 212 213 public synchronized Attributes findOrAddSeriesRecord(Attributes studyRec, Attributes rec) 214 throws IOException { 215 Attributes seriesRec = super.findSeriesRecord(studyRec, rec.getString(Tag.SeriesInstanceUID)); 216 return seriesRec != null ? seriesRec : addLowerDirectoryRecord(studyRec, rec); 217 } 218 219 public synchronized boolean deleteRecord(Attributes rec) 220 throws IOException { 221 if (rec.getInt(Tag.RecordInUseFlag, 0) == INACTIVE) 222 return false; // already disabled 223 224 for (Attributes lowerRec = readLowerDirectoryRecord(rec); 225 lowerRec != null; 226 lowerRec = readNextDirectoryRecord(lowerRec)) 227 deleteRecord(lowerRec); 228 229 rec.setInt(Tag.RecordInUseFlag, VR.US, INACTIVE); 230 markAsDirty(rec); 231 return true; 232 } 233 234 public synchronized void rollback() throws IOException { 235 if (dirtyRecords.isEmpty()) 236 return; 237 238 clearCache(); 239 dirtyRecords.clear(); 240 if (rollbackLen != -1) { 241 restoreDirInfo(); 242 nextRecordPos = rollbackLen; 243 if (getEncodingOptions().undefSequenceLength) { 244 writeSequenceDelimitationItem(); 245 raf.setLength(raf.getFilePointer()); 246 } else { 247 raf.setLength(rollbackLen); 248 } 249 writeFileSetConsistencyFlag(NO_KNOWN_INCONSISTENCIES); 250 rollbackLen = -1; 251 } 252 } 253 254 public void clearCache() { 255 lastChildRecords.clear(); 256 super.clearCache(); 257 } 258 259 public synchronized void commit() throws IOException { 260 if (dirtyRecords.isEmpty()) 261 return; 262 263 if (rollbackLen == -1) 264 writeFileSetConsistencyFlag(KNOWN_INCONSISTENCIES); 265 266 for (Attributes rec : dirtyRecords) 267 writeDirRecordHeader(rec); 268 269 dirtyRecords.clear(); 270 271 if (rollbackLen != -1 && getEncodingOptions().undefSequenceLength) 272 writeSequenceDelimitationItem(); 273 274 writeDirInfoHeader(); 275 276 rollbackLen = -1; 277 } 278 279 @Override 280 public void close() throws IOException { 281 commit(); 282 super.close(); 283 } 284 285 public String[] toFileIDs(File f) { 286 return toFileIDs(file, f); 287 } 288 289 private static String[] toFileIDs(File dfile, File f) { 290 String dfilepath = dfile.getAbsolutePath(); 291 int dend = dfilepath.lastIndexOf(File.separatorChar) + 1; 292 String dpath = dfilepath.substring(0, dend); 293 String fpath = f.getAbsolutePath(); 294 if (dend == 0 || !fpath.startsWith(dpath)) 295 throw new IllegalArgumentException("file: " + fpath 296 + " not in directory: " + dfile.getAbsoluteFile()); 297 return StringUtils.split(fpath.substring(dend), File.separatorChar); 298 } 299 300 private void updateDirInfoHeader() { 301 ByteUtils.intToBytesLE( 302 getOffsetOfFirstRootDirectoryRecord(), 303 dirInfoHeader, 8); 304 ByteUtils.intToBytesLE( 305 getOffsetOfLastRootDirectoryRecord(), 306 dirInfoHeader, 20); 307 ByteUtils.intToBytesLE( 308 getEncodingOptions().undefSequenceLength 309 ? -1 : nextRecordPos - firstRecordPos, 310 dirInfoHeader, 42); 311 } 312 313 private void restoreDirInfo() { 314 setOffsetOfFirstRootDirectoryRecord( 315 ByteUtils.bytesToIntLE(dirInfoHeader, 8)); 316 setOffsetOfLastRootDirectoryRecord( 317 ByteUtils.bytesToIntLE(dirInfoHeader, 20)); 318 } 319 320 private void writeDirInfoHeader() throws IOException { 321 updateDirInfoHeader(); 322 raf.seek(firstRecordPos - dirInfoHeader.length); 323 raf.write(dirInfoHeader); 324 } 325 326 private void writeDirRecordHeader(Attributes rec) throws IOException { 327 ByteUtils.intToBytesLE( 328 rec.getInt(Tag.OffsetOfTheNextDirectoryRecord, 0), 329 dirRecordHeader, 8); 330 ByteUtils.shortToBytesLE( 331 rec.getInt(Tag.RecordInUseFlag, 0), 332 dirRecordHeader, 20); 333 ByteUtils.intToBytesLE( 334 rec.getInt(Tag.OffsetOfReferencedLowerLevelDirectoryEntity, 0), 335 dirRecordHeader, 30); 336 raf.seek(rec.getItemPosition() + 8); 337 raf.write(dirRecordHeader); 338 } 339 340 private void writeSequenceDelimitationItem() throws IOException { 341 raf.seek(nextRecordPos); 342 out.writeHeader(Tag.SequenceDelimitationItem, null, 0); 343 } 344 345 private void addRecord(int tag, Attributes prevRec, Attributes rec) 346 throws IOException { 347 prevRec.setInt(tag, VR.UL, nextRecordPos); 348 markAsDirty(prevRec); 349 writeRecord(nextRecordPos, rec); 350 } 351 352 private void writeRecord(int offset, Attributes rec) throws IOException { 353 if (LOG.isInfoEnabled()) 354 LOG.info("M-UPDATE {}: add {} Record", file, 355 rec.getString(Tag.DirectoryRecordType, null)); 356 LOG.debug("Directory Record:\n{}", rec); 357 rec.setItemPosition(offset); 358 if (rollbackLen == -1) { 359 rollbackLen = offset; 360 writeFileSetConsistencyFlag(KNOWN_INCONSISTENCIES); 361 } 362 raf.seek(offset); 363 rec.setInt(Tag.OffsetOfTheNextDirectoryRecord, VR.UL, 0); 364 rec.setInt(Tag.RecordInUseFlag, VR.US, IN_USE); 365 rec.setInt(Tag.OffsetOfReferencedLowerLevelDirectoryEntity, VR.UL, 0); 366 rec.writeItemTo(out); 367 nextRecordPos = (int) raf.getFilePointer(); 368 cache.put(offset, rec); 369 } 370 371 private void writeFileSetConsistencyFlag(int flag) throws IOException { 372 raf.seek(firstRecordPos - 14); 373 raf.writeShort(flag); 374 setFileSetConsistencyFlag(flag); 375 } 376 377 private static final Comparator<Attributes> offsetComparator = 378 new Comparator<Attributes>() { 379 public int compare(Attributes item1, Attributes item2) { 380 long d = item1.getItemPosition() - item2.getItemPosition(); 381 return d < 0 ? -1 : d > 0 ? 1 : 0; 382 } 383 }; 384 385 private void markAsDirty(Attributes rec) { 386 int index = Collections.binarySearch(dirtyRecords, rec, offsetComparator); 387 if (index < 0) 388 dirtyRecords.add(-(index + 1), rec); 389 } 390 391 public synchronized int purge() throws IOException { 392 int[] count = { 0 }; 393 purge(findFirstRootDirectoryRecordInUse(false), count); 394 return count[0]; 395 } 396 397 private boolean purge(Attributes rec, int[] count) throws IOException { 398 boolean purge = true; 399 while (rec != null) { 400 if (purge(findLowerDirectoryRecordInUse(rec, false), count) 401 && !rec.containsValue(Tag.ReferencedFileID)) { 402 deleteRecord(rec); 403 count[0]++; 404 } else 405 purge = false; 406 rec = readNextDirectoryRecord(rec); 407 } 408 return purge; 409 } 410}