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.Closeable;
042import java.io.File;
043import java.io.IOException;
044import java.io.RandomAccessFile;
045
046import org.dcm4che3.data.Tag;
047import org.dcm4che3.data.Attributes;
048import org.dcm4che3.data.VR;
049import org.dcm4che3.io.DicomInputStream;
050import org.dcm4che3.io.RAFInputStreamAdapter;
051import org.dcm4che3.util.IntHashMap;
052import org.dcm4che3.util.SafeClose;
053import org.dcm4che3.util.StringUtils;
054
055/**
056 * @author Gunter Zeilinger <gunterze@gmail.com>
057 */
058public class DicomDirReader implements Closeable {
059
060    protected final File file;
061    protected final RandomAccessFile raf;
062    protected final DicomInputStream in;
063    protected final Attributes fmi;
064    protected final Attributes fsInfo;
065    protected final IntHashMap<Attributes> cache = new IntHashMap<Attributes>();
066
067    public DicomDirReader(File file) throws IOException {
068        this(file, "r");
069    }
070
071    protected DicomDirReader(File file, String mode) throws IOException {
072        this.file = file;
073        this.raf = new RandomAccessFile(file, mode);
074        try {
075            this.in = new DicomInputStream(new RAFInputStreamAdapter(raf));
076            this.fmi = in.readFileMetaInformation();
077            this.fsInfo = in.readDataset(-1, Tag.DirectoryRecordSequence);
078            if (in.tag() != Tag.DirectoryRecordSequence)
079                throw new IOException("Missing Directory Record Sequence");
080        } catch (IOException e) {
081            SafeClose.close(raf);
082            throw e;
083        }
084    }
085
086    public final File getFile() {
087        return file;
088    }
089
090    public final Attributes getFileMetaInformation() {
091        return fmi;
092    }
093
094    public final Attributes getFileSetInformation() {
095        return fsInfo;
096    }
097
098    public void close() throws IOException {
099        raf.close();
100    }
101
102    public String getFileSetUID() {
103        return fmi.getString(Tag.MediaStorageSOPInstanceUID, null);
104    }
105
106    public String getTransferSyntaxUID() {
107        return fmi.getString(Tag.TransferSyntaxUID, null);
108    }
109
110    public String getFileSetID() {
111        return fsInfo.getString(Tag.FileSetID, null);
112    }
113
114    public File getDescriptorFile() {
115        return toFile(fsInfo.getStrings(Tag.FileSetDescriptorFileID));
116    }
117
118    public File toFile(String[] fileIDs) {
119        if (fileIDs == null || fileIDs.length == 0)
120            return null;
121
122        return new File(file.getParent(),
123                StringUtils.concat(fileIDs, File.separatorChar));
124    }
125
126    public String getDescriptorFileCharacterSet() {
127        return fsInfo.getString(
128                Tag.SpecificCharacterSetOfFileSetDescriptorFile, null);
129    }
130
131    public int getFileSetConsistencyFlag() {
132        return fsInfo.getInt(Tag.FileSetConsistencyFlag, 0);
133    }
134
135    protected void setFileSetConsistencyFlag(int i) {
136        fsInfo.setInt(Tag.FileSetConsistencyFlag, VR.US, i);
137    }
138
139    public boolean knownInconsistencies() {
140        return getFileSetConsistencyFlag() != 0;
141    }
142
143    public int getOffsetOfFirstRootDirectoryRecord() {
144        return fsInfo.getInt(
145                Tag.OffsetOfTheFirstDirectoryRecordOfTheRootDirectoryEntity, 0);
146    }
147
148    protected void setOffsetOfFirstRootDirectoryRecord(int i) {
149        fsInfo.setInt(
150                Tag.OffsetOfTheFirstDirectoryRecordOfTheRootDirectoryEntity,
151                VR.UL, i);
152    }
153
154    public int getOffsetOfLastRootDirectoryRecord() {
155        return fsInfo.getInt(
156                Tag.OffsetOfTheLastDirectoryRecordOfTheRootDirectoryEntity, 0);
157    }
158
159    protected void setOffsetOfLastRootDirectoryRecord(int i) {
160        fsInfo.setInt(
161                Tag.OffsetOfTheLastDirectoryRecordOfTheRootDirectoryEntity,
162                VR.UL, i);
163    }
164
165    public boolean isEmpty() {
166        return getOffsetOfFirstRootDirectoryRecord() == 0;
167    }
168
169    public void clearCache() {
170        cache.clear();
171    }
172
173    public Attributes readFirstRootDirectoryRecord() throws IOException {
174        return readRecord(getOffsetOfFirstRootDirectoryRecord());
175    }
176
177    public Attributes readLastRootDirectoryRecord() throws IOException {
178        return readRecord(getOffsetOfLastRootDirectoryRecord());
179    }
180
181    public Attributes readNextDirectoryRecord(Attributes rec)
182            throws IOException {
183        return readRecord(
184                rec.getInt(Tag.OffsetOfTheNextDirectoryRecord, 0));
185    }
186
187    public Attributes readLowerDirectoryRecord(Attributes rec)
188            throws IOException {
189        return readRecord(
190                rec.getInt(Tag.OffsetOfReferencedLowerLevelDirectoryEntity, 0));
191    }
192
193    protected Attributes findLastLowerDirectoryRecord(Attributes rec)
194            throws IOException {
195        Attributes lower = readLowerDirectoryRecord(rec);
196        if (lower == null)
197            return null;
198
199        Attributes next;
200        while ((next = readNextDirectoryRecord(lower)) != null)
201            lower = next;
202        return lower;
203    }
204
205    public Attributes findFirstRootDirectoryRecordInUse(boolean ignorePrivate) throws IOException {
206        return findRootDirectoryRecord(ignorePrivate, null, false, false);
207    }
208
209    public Attributes findRootDirectoryRecord(Attributes keys, boolean ignorePrivate,
210            boolean ignoreCaseOfPN, boolean matchNoValue)
211            throws IOException {
212        return findRecordInUse(getOffsetOfFirstRootDirectoryRecord(), ignorePrivate,
213                keys, ignoreCaseOfPN, matchNoValue);
214    }
215
216    public Attributes findRootDirectoryRecord(boolean ignorePrivate, Attributes keys,
217            boolean ignoreCaseOfPN, boolean matchNoValue) throws IOException {
218        return findRootDirectoryRecord(keys, ignorePrivate, ignoreCaseOfPN, matchNoValue);
219    }
220
221    public Attributes findNextDirectoryRecordInUse(Attributes rec, boolean ignorePrivate)
222            throws IOException {
223        return findNextDirectoryRecord(rec, ignorePrivate, null, false, false);
224    }
225
226    public Attributes findNextDirectoryRecord(Attributes rec, boolean ignorePrivate,
227            Attributes keys, boolean ignoreCaseOfPN, boolean matchNoValue) throws IOException {
228        return findRecordInUse(
229                rec.getInt(Tag.OffsetOfTheNextDirectoryRecord, 0), ignorePrivate,
230                keys, ignoreCaseOfPN, matchNoValue);
231    }
232
233    public Attributes findLowerDirectoryRecordInUse(Attributes rec, boolean ignorePrivate)
234            throws IOException {
235        return findLowerDirectoryRecord(rec, ignorePrivate, null, false, false);
236    }
237
238    public Attributes findLowerDirectoryRecord(Attributes rec, boolean ignorePrivate,
239            Attributes keys, boolean ignoreCaseOfPN, boolean matchNoValue)
240            throws IOException {
241        return findRecordInUse(
242                rec.getInt(Tag.OffsetOfReferencedLowerLevelDirectoryEntity, 0), ignorePrivate,
243                keys, ignoreCaseOfPN, matchNoValue);
244    }
245
246    public Attributes findPatientRecord(String... ids) throws IOException {
247        return findRootDirectoryRecord(false,
248                pk("PATIENT", Tag.PatientID, VR.LO, ids), false, false);
249    }
250
251    public Attributes findNextPatientRecord(Attributes patRec, String... ids) throws IOException {
252        return findNextDirectoryRecord(patRec, false,
253                pk("PATIENT", Tag.PatientID, VR.LO, ids), false, false);
254    }
255
256    public Attributes findStudyRecord(Attributes patRec, String... iuids)
257            throws IOException {
258        return findLowerDirectoryRecord(patRec, false,
259                pk("STUDY", Tag.StudyInstanceUID, VR.UI, iuids),
260                false, false);
261    }
262
263    public Attributes findNextStudyRecord(Attributes studyRec, String... iuids)
264            throws IOException {
265        return findNextDirectoryRecord(studyRec, false,
266                pk("STUDY", Tag.StudyInstanceUID, VR.UI, iuids),
267                false, false);
268    }
269
270    public Attributes findSeriesRecord(Attributes studyRec, String... iuids)
271            throws IOException {
272        return findLowerDirectoryRecord(studyRec, false, 
273                pk("SERIES", Tag.SeriesInstanceUID, VR.UI, iuids),
274                false, false);
275    }
276
277    public Attributes findNextSeriesRecord(Attributes seriesRec, String... iuids)
278            throws IOException {
279        return findNextDirectoryRecord(seriesRec, false, 
280                pk("SERIES", Tag.SeriesInstanceUID, VR.UI, iuids),
281                false, false);
282    }
283
284    public Attributes findLowerInstanceRecord(Attributes seriesRec, boolean ignorePrivate,
285            String... iuids) throws IOException {
286        return findLowerDirectoryRecord(seriesRec, ignorePrivate, pk(iuids), false, false);
287    }
288
289    public Attributes findNextInstanceRecord(Attributes instRec, boolean ignorePrivate,
290            String... iuids) throws IOException {
291        return findNextDirectoryRecord(instRec, ignorePrivate, pk(iuids), false, false);
292    }
293
294    public Attributes findRootInstanceRecord(boolean ignorePrivate, String... iuids)
295            throws IOException {
296        return findRootDirectoryRecord(ignorePrivate, pk(iuids), false, false);
297    }
298
299    private Attributes pk(String type, int tag, VR vr, String... ids) {
300        Attributes pk = new Attributes(2);
301        pk.setString(Tag.DirectoryRecordType, VR.CS, type);
302        if (ids != null && ids.length != 0)
303            pk.setString(tag, vr, ids);
304        return pk;
305    }
306
307    private Attributes pk(String... iuids) {
308        if (iuids == null || iuids.length == 0)
309            return null;
310
311        Attributes pk = new Attributes(1);
312        pk.setString(Tag.ReferencedSOPInstanceUIDInFile, VR.UI, iuids);
313        return pk;
314    }
315
316    private Attributes findRecordInUse(int offset, boolean ignorePrivate, Attributes keys,
317            boolean ignoreCaseOfPN, boolean matchNoValue)
318            throws IOException {
319        while (offset != 0) {
320            Attributes item = readRecord(offset);
321            if (inUse(item) && !(ignorePrivate && isPrivate(item))
322                    && (keys == null || item.matches(keys, ignoreCaseOfPN, matchNoValue)))
323                return item;
324            offset = item.getInt(Tag.OffsetOfTheNextDirectoryRecord, 0);
325        }
326        return null;
327    }
328
329    private synchronized Attributes readRecord(int offset) throws IOException {
330        if (offset == 0)
331            return null;
332
333        Attributes item = cache.get(offset);
334        if (item == null) {
335            long off = offset & 0xffffffffL;
336            raf.seek(off);
337            in.setPosition(off);
338            item = in.readItem();
339            cache.put(offset, item);
340        }
341        return item;
342    }
343
344    public static boolean inUse(Attributes rec) {
345        return rec.getInt(Tag.RecordInUseFlag, 0) != 0;
346    }
347
348    public static boolean isPrivate(Attributes rec) {
349        return "PRIVATE".equals(rec.getString(Tag.DirectoryRecordType));
350    }
351
352}