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}